From 1da849ac48f7a327c1b78510de96c338d762132d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 25 Aug 2021 15:43:53 +0200 Subject: [PATCH 001/906] =?UTF-8?q?=F0=9F=8E=A8=20Fix=20type=20detection?= =?UTF-8?q?=20of=20select=20results=20in=20PyCharm=20(#15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/orm/session.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/sqlmodel/orm/session.py b/sqlmodel/orm/session.py index d708f1e7e9..a5a63e2c69 100644 --- a/sqlmodel/orm/session.py +++ b/sqlmodel/orm/session.py @@ -10,14 +10,14 @@ from ..engine.result import Result, ScalarResult from ..sql.base import Executable -_T = TypeVar("_T") +_TSelectParam = TypeVar("_TSelectParam") class Session(_Session): @overload def exec( self, - statement: Select[_T], + statement: Select[_TSelectParam], *, params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, execution_options: Mapping[str, Any] = util.EMPTY_DICT, @@ -25,13 +25,13 @@ def exec( _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, **kw: Any, - ) -> Union[Result[_T]]: + ) -> Result[_TSelectParam]: ... @overload def exec( self, - statement: SelectOfScalar[_T], + statement: SelectOfScalar[_TSelectParam], *, params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, execution_options: Mapping[str, Any] = util.EMPTY_DICT, @@ -39,12 +39,16 @@ def exec( _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, **kw: Any, - ) -> Union[ScalarResult[_T]]: + ) -> ScalarResult[_TSelectParam]: ... def exec( self, - statement: Union[Select[_T], SelectOfScalar[_T], Executable[_T]], + statement: Union[ + Select[_TSelectParam], + SelectOfScalar[_TSelectParam], + Executable[_TSelectParam], + ], *, params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, execution_options: Mapping[str, Any] = util.EMPTY_DICT, @@ -52,7 +56,7 @@ def exec( _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, **kw: Any, - ) -> Union[Result[_T], ScalarResult[_T]]: + ) -> Union[Result[_TSelectParam], ScalarResult[_TSelectParam]]: results = super().execute( statement, params=params, @@ -118,13 +122,13 @@ def query(self, *entities: Any, **kwargs: Any) -> "_Query[Any]": def get( self, - entity: Type[_T], + entity: Type[_TSelectParam], ident: Any, options: Optional[Sequence[Any]] = None, populate_existing: bool = False, with_for_update: Optional[Union[Literal[True], Mapping[str, Any]]] = None, identity_token: Optional[Any] = None, - ) -> Optional[_T]: + ) -> Optional[_TSelectParam]: return super().get( entity, ident, From 878e2307823cdbd25550df0b708a3a19d2c65cc6 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 25 Aug 2021 13:44:35 +0000 Subject: [PATCH 002/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 31c3c7af10..936776ba8d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🎨 Fix type detection of select results in PyCharm. PR [#15](https://github.com/tiangolo/sqlmodel/pull/15) by [@tiangolo](https://github.com/tiangolo). ## 0.0.3 From 02da85c9ec39b0ebb7ff91de3045e0aea28dc998 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 25 Aug 2021 15:46:57 +0200 Subject: [PATCH 003/906] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.0.?= =?UTF-8?q?4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 3 +++ sqlmodel/__init__.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 936776ba8d..d27692deab 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.0.4 + * 🎨 Fix type detection of select results in PyCharm. PR [#15](https://github.com/tiangolo/sqlmodel/pull/15) by [@tiangolo](https://github.com/tiangolo). ## 0.0.3 diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index 3352f91a4e..bbe86ee2ae 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.0.3" +__version__ = "0.0.4" # Re-export from SQLAlchemy from sqlalchemy.engine import create_mock_engine as create_mock_engine From e30c7ef4e95aea4febbbb51241a03036872d7920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 30 Nov 2021 17:12:28 +0100 Subject: [PATCH 004/906] =?UTF-8?q?=E2=9C=A8=20Update=20type=20annotations?= =?UTF-8?q?=20and=20upgrade=20mypy=20(#173)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 6 ++- sqlmodel/engine/create.py | 2 +- sqlmodel/engine/result.py | 8 ++-- sqlmodel/ext/asyncio/session.py | 4 +- sqlmodel/main.py | 73 ++++++++++++++++++------------- sqlmodel/orm/session.py | 6 +-- sqlmodel/sql/base.py | 4 +- sqlmodel/sql/expression.py | 32 +++++++------- sqlmodel/sql/expression.py.jinja2 | 12 ++--- sqlmodel/sql/sqltypes.py | 19 ++++---- 10 files changed, 90 insertions(+), 76 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fc567909a8..a8355cf1ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ sqlalchemy2-stubs = {version = "*", allow-prereleases = true} [tool.poetry.dev-dependencies] pytest = "^6.2.4" -mypy = "^0.812" +mypy = "^0.910" flake8 = "^3.9.2" black = {version = "^21.5-beta.1", python = "^3.7"} mkdocs = "^1.2.1" @@ -98,3 +98,7 @@ warn_return_any = true implicit_reexport = false strict_equality = true # --strict end + +[[tool.mypy.overrides]] +module = "sqlmodel.sql.expression" +warn_unused_ignores = false diff --git a/sqlmodel/engine/create.py b/sqlmodel/engine/create.py index 97481259e2..b2d567b1b1 100644 --- a/sqlmodel/engine/create.py +++ b/sqlmodel/engine/create.py @@ -136,4 +136,4 @@ def create_engine( if not isinstance(query_cache_size, _DefaultPlaceholder): current_kwargs["query_cache_size"] = query_cache_size current_kwargs.update(kwargs) - return _create_engine(url, **current_kwargs) + return _create_engine(url, **current_kwargs) # type: ignore diff --git a/sqlmodel/engine/result.py b/sqlmodel/engine/result.py index d521427581..7a25422227 100644 --- a/sqlmodel/engine/result.py +++ b/sqlmodel/engine/result.py @@ -23,7 +23,7 @@ def __iter__(self) -> Iterator[_T]: return super().__iter__() def __next__(self) -> _T: - return super().__next__() + return super().__next__() # type: ignore def first(self) -> Optional[_T]: return super().first() @@ -32,7 +32,7 @@ def one_or_none(self) -> Optional[_T]: return super().one_or_none() def one(self) -> _T: - return super().one() + return super().one() # type: ignore class Result(_Result, Generic[_T]): @@ -70,10 +70,10 @@ def scalar_one(self) -> _T: return super().scalar_one() # type: ignore def scalar_one_or_none(self) -> Optional[_T]: - return super().scalar_one_or_none() # type: ignore + return super().scalar_one_or_none() def one(self) -> _T: # type: ignore return super().one() # type: ignore def scalar(self) -> Optional[_T]: - return super().scalar() # type: ignore + return super().scalar() diff --git a/sqlmodel/ext/asyncio/session.py b/sqlmodel/ext/asyncio/session.py index 40e5b766e9..80267b25e5 100644 --- a/sqlmodel/ext/asyncio/session.py +++ b/sqlmodel/ext/asyncio/session.py @@ -21,7 +21,7 @@ def __init__( self, bind: Optional[Union[AsyncConnection, AsyncEngine]] = None, binds: Optional[Mapping[object, Union[AsyncConnection, AsyncEngine]]] = None, - **kw, + **kw: Any, ): # All the same code of the original AsyncSession kw["future"] = True @@ -52,7 +52,7 @@ async def exec( # util.immutabledict has the union() method. Is this a bug in SQLAlchemy? execution_options = execution_options.union({"prebuffer_rows": True}) # type: ignore - return await greenlet_spawn( # type: ignore + return await greenlet_spawn( self.sync_session.exec, statement, params=params, diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 661276b31d..84e26c4532 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -101,7 +101,7 @@ def __init__( *, back_populates: Optional[str] = None, link_model: Optional[Any] = None, - sa_relationship: Optional[RelationshipProperty] = None, + sa_relationship: Optional[RelationshipProperty] = None, # type: ignore sa_relationship_args: Optional[Sequence[Any]] = None, sa_relationship_kwargs: Optional[Mapping[str, Any]] = None, ) -> None: @@ -127,32 +127,32 @@ def Field( default: Any = Undefined, *, default_factory: Optional[NoArgAnyCallable] = None, - alias: str = None, - title: str = None, - description: str = None, + alias: Optional[str] = None, + title: Optional[str] = None, + description: Optional[str] = None, exclude: Union[ AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any ] = None, include: Union[ AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any ] = None, - const: bool = None, - gt: float = None, - ge: float = None, - lt: float = None, - le: float = None, - multiple_of: float = None, - min_items: int = None, - max_items: int = None, - min_length: int = None, - max_length: int = None, + const: Optional[bool] = None, + gt: Optional[float] = None, + ge: Optional[float] = None, + lt: Optional[float] = None, + le: Optional[float] = None, + multiple_of: Optional[float] = None, + min_items: Optional[int] = None, + max_items: Optional[int] = None, + min_length: Optional[int] = None, + max_length: Optional[int] = None, allow_mutation: bool = True, - regex: str = None, + regex: Optional[str] = None, primary_key: bool = False, foreign_key: Optional[Any] = None, nullable: Union[bool, UndefinedType] = Undefined, index: Union[bool, UndefinedType] = Undefined, - sa_column: Union[Column, UndefinedType] = Undefined, + sa_column: Union[Column, UndefinedType] = Undefined, # type: ignore sa_column_args: Union[Sequence[Any], UndefinedType] = Undefined, sa_column_kwargs: Union[Mapping[str, Any], UndefinedType] = Undefined, schema_extra: Optional[Dict[str, Any]] = None, @@ -195,7 +195,7 @@ def Relationship( *, back_populates: Optional[str] = None, link_model: Optional[Any] = None, - sa_relationship: Optional[RelationshipProperty] = None, + sa_relationship: Optional[RelationshipProperty] = None, # type: ignore sa_relationship_args: Optional[Sequence[Any]] = None, sa_relationship_kwargs: Optional[Mapping[str, Any]] = None, ) -> Any: @@ -217,19 +217,25 @@ class SQLModelMetaclass(ModelMetaclass, DeclarativeMeta): # Replicate SQLAlchemy def __setattr__(cls, name: str, value: Any) -> None: - if getattr(cls.__config__, "table", False): # type: ignore + if getattr(cls.__config__, "table", False): DeclarativeMeta.__setattr__(cls, name, value) else: super().__setattr__(name, value) def __delattr__(cls, name: str) -> None: - if getattr(cls.__config__, "table", False): # type: ignore + if getattr(cls.__config__, "table", False): DeclarativeMeta.__delattr__(cls, name) else: super().__delattr__(name) # From Pydantic - def __new__(cls, name, bases, class_dict: dict, **kwargs) -> Any: + def __new__( + cls, + name: str, + bases: Tuple[Type[Any], ...], + class_dict: Dict[str, Any], + **kwargs: Any, + ) -> Any: relationships: Dict[str, RelationshipInfo] = {} dict_for_pydantic = {} original_annotations = resolve_annotations( @@ -342,7 +348,7 @@ def __init__( ) relationship_to = temp_field.type_ if isinstance(temp_field.type_, ForwardRef): - relationship_to = temp_field.type_.__forward_arg__ # type: ignore + relationship_to = temp_field.type_.__forward_arg__ rel_kwargs: Dict[str, Any] = {} if rel_info.back_populates: rel_kwargs["back_populates"] = rel_info.back_populates @@ -360,7 +366,7 @@ def __init__( rel_args.extend(rel_info.sa_relationship_args) if rel_info.sa_relationship_kwargs: rel_kwargs.update(rel_info.sa_relationship_kwargs) - rel_value: RelationshipProperty = relationship( + rel_value: RelationshipProperty = relationship( # type: ignore relationship_to, *rel_args, **rel_kwargs ) dict_used[rel_name] = rel_value @@ -408,7 +414,7 @@ def get_sqlachemy_type(field: ModelField) -> Any: return GUID -def get_column_from_field(field: ModelField) -> Column: +def get_column_from_field(field: ModelField) -> Column: # type: ignore sa_column = getattr(field.field_info, "sa_column", Undefined) if isinstance(sa_column, Column): return sa_column @@ -440,10 +446,10 @@ def get_column_from_field(field: ModelField) -> Column: kwargs["default"] = sa_default sa_column_args = getattr(field.field_info, "sa_column_args", Undefined) if sa_column_args is not Undefined: - args.extend(list(cast(Sequence, sa_column_args))) + args.extend(list(cast(Sequence[Any], sa_column_args))) sa_column_kwargs = getattr(field.field_info, "sa_column_kwargs", Undefined) if sa_column_kwargs is not Undefined: - kwargs.update(cast(dict, sa_column_kwargs)) + kwargs.update(cast(Dict[Any, Any], sa_column_kwargs)) return Column(sa_type, *args, **kwargs) @@ -452,24 +458,27 @@ def get_column_from_field(field: ModelField) -> Column: default_registry = registry() -def _value_items_is_true(v) -> bool: +def _value_items_is_true(v: Any) -> bool: # Re-implement Pydantic's ValueItems.is_true() as it hasn't been released as of # the current latest, Pydantic 1.8.2 return v is True or v is ... +_TSQLModel = TypeVar("_TSQLModel", bound="SQLModel") + + class SQLModel(BaseModel, metaclass=SQLModelMetaclass, registry=default_registry): # SQLAlchemy needs to set weakref(s), Pydantic will set the other slots values __slots__ = ("__weakref__",) __tablename__: ClassVar[Union[str, Callable[..., str]]] - __sqlmodel_relationships__: ClassVar[Dict[str, RelationshipProperty]] + __sqlmodel_relationships__: ClassVar[Dict[str, RelationshipProperty]] # type: ignore __name__: ClassVar[str] metadata: ClassVar[MetaData] class Config: orm_mode = True - def __new__(cls, *args, **kwargs) -> Any: + def __new__(cls, *args: Any, **kwargs: Any) -> Any: new_object = super().__new__(cls) # SQLAlchemy doesn't call __init__ on the base class # Ref: https://docs.sqlalchemy.org/en/14/orm/constructors.html @@ -520,7 +529,9 @@ def __setattr__(self, name: str, value: Any) -> None: super().__setattr__(name, value) @classmethod - def from_orm(cls: Type["SQLModel"], obj: Any, update: Dict[str, Any] = None): + def from_orm( + cls: Type[_TSQLModel], obj: Any, update: Optional[Dict[str, Any]] = None + ) -> _TSQLModel: # Duplicated from Pydantic if not cls.__config__.orm_mode: raise ConfigError( @@ -533,7 +544,7 @@ def from_orm(cls: Type["SQLModel"], obj: Any, update: Dict[str, Any] = None): # End SQLModel support dict if not getattr(cls.__config__, "table", False): # If not table, normal Pydantic code - m = cls.__new__(cls) + m: _TSQLModel = cls.__new__(cls) else: # If table, create the new instance normally to make SQLAlchemy create # the _sa_instance_state attribute @@ -554,7 +565,7 @@ def from_orm(cls: Type["SQLModel"], obj: Any, update: Dict[str, Any] = None): @classmethod def parse_obj( - cls: Type["SQLModel"], obj: Any, update: Dict[str, Any] = None + cls: Type["SQLModel"], obj: Any, update: Optional[Dict[str, Any]] = None ) -> "SQLModel": obj = cls._enforce_dict_if_root(obj) # SQLModel, support update dict diff --git a/sqlmodel/orm/session.py b/sqlmodel/orm/session.py index a5a63e2c69..453e0eefaf 100644 --- a/sqlmodel/orm/session.py +++ b/sqlmodel/orm/session.py @@ -60,7 +60,7 @@ def exec( results = super().execute( statement, params=params, - execution_options=execution_options, # type: ignore + execution_options=execution_options, bind_arguments=bind_arguments, _parent_execute_state=_parent_execute_state, _add_event=_add_event, @@ -74,7 +74,7 @@ def execute( self, statement: _Executable, params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, - execution_options: Mapping[str, Any] = util.EMPTY_DICT, + execution_options: Optional[Mapping[str, Any]] = util.EMPTY_DICT, bind_arguments: Optional[Mapping[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, @@ -101,7 +101,7 @@ def execute( return super().execute( # type: ignore statement, params=params, - execution_options=execution_options, # type: ignore + execution_options=execution_options, bind_arguments=bind_arguments, _parent_execute_state=_parent_execute_state, _add_event=_add_event, diff --git a/sqlmodel/sql/base.py b/sqlmodel/sql/base.py index 129e4d43d7..3764a9721d 100644 --- a/sqlmodel/sql/base.py +++ b/sqlmodel/sql/base.py @@ -6,6 +6,4 @@ class Executable(_Executable, Generic[_T]): - def __init__(self, *args, **kwargs): - self.__dict__["_exec_options"] = kwargs.pop("_exec_options", None) - super(_Executable, self).__init__(*args, **kwargs) + pass diff --git a/sqlmodel/sql/expression.py b/sqlmodel/sql/expression.py index 66063bf236..bf6ea38ec6 100644 --- a/sqlmodel/sql/expression.py +++ b/sqlmodel/sql/expression.py @@ -45,10 +45,10 @@ class SelectOfScalar(_Select, Generic[_TSelect]): class GenericSelectMeta(GenericMeta, _Select.__class__): # type: ignore pass - class _Py36Select(_Select, Generic[_TSelect], metaclass=GenericSelectMeta): # type: ignore + class _Py36Select(_Select, Generic[_TSelect], metaclass=GenericSelectMeta): pass - class _Py36SelectOfScalar(_Select, Generic[_TSelect], metaclass=GenericSelectMeta): # type: ignore + class _Py36SelectOfScalar(_Select, Generic[_TSelect], metaclass=GenericSelectMeta): pass # Cast them for editors to work correctly, from several tricks tried, this works @@ -65,9 +65,9 @@ class _Py36SelectOfScalar(_Select, Generic[_TSelect], metaclass=GenericSelectMet _TScalar_0 = TypeVar( "_TScalar_0", - Column, - Sequence, - Mapping, + Column, # type: ignore + Sequence, # type: ignore + Mapping, # type: ignore UUID, datetime, float, @@ -83,9 +83,9 @@ class _Py36SelectOfScalar(_Select, Generic[_TSelect], metaclass=GenericSelectMet _TScalar_1 = TypeVar( "_TScalar_1", - Column, - Sequence, - Mapping, + Column, # type: ignore + Sequence, # type: ignore + Mapping, # type: ignore UUID, datetime, float, @@ -101,9 +101,9 @@ class _Py36SelectOfScalar(_Select, Generic[_TSelect], metaclass=GenericSelectMet _TScalar_2 = TypeVar( "_TScalar_2", - Column, - Sequence, - Mapping, + Column, # type: ignore + Sequence, # type: ignore + Mapping, # type: ignore UUID, datetime, float, @@ -119,9 +119,9 @@ class _Py36SelectOfScalar(_Select, Generic[_TSelect], metaclass=GenericSelectMet _TScalar_3 = TypeVar( "_TScalar_3", - Column, - Sequence, - Mapping, + Column, # type: ignore + Sequence, # type: ignore + Mapping, # type: ignore UUID, datetime, float, @@ -446,14 +446,14 @@ def select( # type: ignore # Generated overloads end -def select(*entities: Any, **kw: Any) -> Union[Select, SelectOfScalar]: +def select(*entities: Any, **kw: Any) -> Union[Select, SelectOfScalar]: # type: ignore if len(entities) == 1: return SelectOfScalar._create(*entities, **kw) # type: ignore return Select._create(*entities, **kw) # type: ignore # TODO: add several @overload from Python types to SQLAlchemy equivalents -def col(column_expression: Any) -> ColumnClause: +def col(column_expression: Any) -> ColumnClause: # type: ignore if not isinstance(column_expression, (ColumnClause, Column, InstrumentedAttribute)): raise RuntimeError(f"Not a SQLAlchemy column: {column_expression}") return column_expression diff --git a/sqlmodel/sql/expression.py.jinja2 b/sqlmodel/sql/expression.py.jinja2 index b39d636ea2..9cd5d3f33e 100644 --- a/sqlmodel/sql/expression.py.jinja2 +++ b/sqlmodel/sql/expression.py.jinja2 @@ -63,9 +63,9 @@ if TYPE_CHECKING: # pragma: no cover {% for i in range(number_of_types) %} _TScalar_{{ i }} = TypeVar( "_TScalar_{{ i }}", - Column, - Sequence, - Mapping, + Column, # type: ignore + Sequence, # type: ignore + Mapping, # type: ignore UUID, datetime, float, @@ -106,14 +106,14 @@ def select( # type: ignore # Generated overloads end -def select(*entities: Any, **kw: Any) -> Union[Select, SelectOfScalar]: +def select(*entities: Any, **kw: Any) -> Union[Select, SelectOfScalar]: # type: ignore if len(entities) == 1: return SelectOfScalar._create(*entities, **kw) # type: ignore - return Select._create(*entities, **kw) + return Select._create(*entities, **kw) # type: ignore # TODO: add several @overload from Python types to SQLAlchemy equivalents -def col(column_expression: Any) -> ColumnClause: +def col(column_expression: Any) -> ColumnClause: # type: ignore if not isinstance(column_expression, (ColumnClause, Column, InstrumentedAttribute)): raise RuntimeError(f"Not a SQLAlchemy column: {column_expression}") return column_expression diff --git a/sqlmodel/sql/sqltypes.py b/sqlmodel/sql/sqltypes.py index e7b77b8c52..b3fda87739 100644 --- a/sqlmodel/sql/sqltypes.py +++ b/sqlmodel/sql/sqltypes.py @@ -1,13 +1,14 @@ import uuid -from typing import Any, cast +from typing import Any, Optional, cast from sqlalchemy import types from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.engine.interfaces import Dialect +from sqlalchemy.sql.type_api import TypeEngine from sqlalchemy.types import CHAR, TypeDecorator -class AutoString(types.TypeDecorator): +class AutoString(types.TypeDecorator): # type: ignore impl = types.String cache_ok = True @@ -22,7 +23,7 @@ def load_dialect_impl(self, dialect: Dialect) -> "types.TypeEngine[Any]": # Reference form SQLAlchemy docs: https://docs.sqlalchemy.org/en/14/core/custom_types.html#backend-agnostic-guid-type # with small modifications -class GUID(TypeDecorator): +class GUID(TypeDecorator): # type: ignore """Platform-independent GUID type. Uses PostgreSQL's UUID type, otherwise uses @@ -33,13 +34,13 @@ class GUID(TypeDecorator): impl = CHAR cache_ok = True - def load_dialect_impl(self, dialect): + def load_dialect_impl(self, dialect: Dialect) -> TypeEngine: # type: ignore if dialect.name == "postgresql": - return dialect.type_descriptor(UUID()) + return dialect.type_descriptor(UUID()) # type: ignore else: - return dialect.type_descriptor(CHAR(32)) + return dialect.type_descriptor(CHAR(32)) # type: ignore - def process_bind_param(self, value, dialect): + def process_bind_param(self, value: Any, dialect: Dialect) -> Optional[str]: if value is None: return value elif dialect.name == "postgresql": @@ -51,10 +52,10 @@ def process_bind_param(self, value, dialect): # hexstring return f"{value.int:x}" - def process_result_value(self, value, dialect): + def process_result_value(self, value: Any, dialect: Dialect) -> Optional[uuid.UUID]: if value is None: return value else: if not isinstance(value, uuid.UUID): value = uuid.UUID(value) - return value + return cast(uuid.UUID, value) From 328c8c725d4889d9d389b016ff850a5600d2f16a Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 30 Nov 2021 16:13:10 +0000 Subject: [PATCH 005/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index d27692deab..2ae1e5f968 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Update type annotations and upgrade mypy. PR [#173](https://github.com/tiangolo/sqlmodel/pull/173) by [@tiangolo](https://github.com/tiangolo). ## 0.0.4 From 55259b3c8b66ad45f65f40cafc28f26edd9acb43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 30 Nov 2021 17:27:50 +0100 Subject: [PATCH 006/906] =?UTF-8?q?=F0=9F=94=A7=20Add=20MkDocs=20Material?= =?UTF-8?q?=20social=20cards=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 4 ++++ .gitignore | 1 + mkdocs.yml | 1 + 3 files changed, 6 insertions(+) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 82402f537a..31b799225c 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -51,6 +51,10 @@ jobs: - name: Install Material for MkDocs Insiders if: github.event.pull_request.head.repo.fork == false && steps.cache.outputs.cache-hit != 'true' run: python -m poetry run pip install git+https://${{ secrets.ACTIONS_TOKEN }}@github.com/squidfunk/mkdocs-material-insiders.git + - uses: actions/cache@v2 + with: + key: mkdocs-cards-${{ github.ref }} + path: .cache - name: Build Docs run: python -m poetry run mkdocs build - name: Zip docs diff --git a/.gitignore b/.gitignore index 909f50ed81..4006069389 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ htmlcov coverage.xml site *.db +.cache diff --git a/mkdocs.yml b/mkdocs.yml index 5ebc361083..673c2d3cf5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -32,6 +32,7 @@ google_analytics: - auto plugins: - search +- social nav: - SQLModel: index.md - features.md From 455794da2c43fddda1c2e0020d58b7ff509c8bcd Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 30 Nov 2021 16:28:31 +0000 Subject: [PATCH 007/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 2ae1e5f968..fd74dae034 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Add MkDocs Material social cards. PR [#90](https://github.com/tiangolo/sqlmodel/pull/90) by [@tiangolo](https://github.com/tiangolo). * ✨ Update type annotations and upgrade mypy. PR [#173](https://github.com/tiangolo/sqlmodel/pull/173) by [@tiangolo](https://github.com/tiangolo). ## 0.0.4 From 82935cae9f49107b17d52ef140f449b5086ed281 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lehoczky=20Zolt=C3=A1n?= Date: Fri, 3 Dec 2021 11:23:20 +0100 Subject: [PATCH 008/906] =?UTF-8?q?=F0=9F=90=9BFix=20docs=20light/dark=20t?= =?UTF-8?q?heme=20switcher=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🐛Fix tooltip text for theme switcher * 🔧 Update lightbulb icon Co-authored-by: Sebastián Ramírez --- mkdocs.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 673c2d3cf5..e5c5cef342 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,14 +8,14 @@ theme: primary: deep purple accent: amber toggle: - icon: material/lightbulb-outline - name: Switch to light mode + icon: material/lightbulb + name: Switch to dark mode - scheme: slate primary: deep purple accent: amber toggle: - icon: material/lightbulb - name: Switch to dark mode + icon: material/lightbulb-outline + name: Switch to light mode features: - search.suggest - search.highlight From a36c6d5778e23bb310c18992b2f301ffb34e8c8e Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 3 Dec 2021 10:24:01 +0000 Subject: [PATCH 009/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index fd74dae034..c794f501d7 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛Fix docs light/dark theme switcher. PR [#1](https://github.com/tiangolo/sqlmodel/pull/1) by [@Lehoczky](https://github.com/Lehoczky). * 🔧 Add MkDocs Material social cards. PR [#90](https://github.com/tiangolo/sqlmodel/pull/90) by [@tiangolo](https://github.com/tiangolo). * ✨ Update type annotations and upgrade mypy. PR [#173](https://github.com/tiangolo/sqlmodel/pull/173) by [@tiangolo](https://github.com/tiangolo). From 362eb81701ec4edec6638532644bf1e823f7ea05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 13 Dec 2021 11:40:40 +0100 Subject: [PATCH 010/906] =?UTF-8?q?=F0=9F=8E=A8=20Format=20expression.py?= =?UTF-8?q?=20and=20expression=20template,=20currently=20needed=20by=20CI?= =?UTF-8?q?=20(#187)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/sql/expression.py | 1 - sqlmodel/sql/expression.py.jinja2 | 1 - 2 files changed, 2 deletions(-) diff --git a/sqlmodel/sql/expression.py b/sqlmodel/sql/expression.py index bf6ea38ec6..e7317bcdd8 100644 --- a/sqlmodel/sql/expression.py +++ b/sqlmodel/sql/expression.py @@ -38,7 +38,6 @@ class Select(_Select, Generic[_TSelect]): class SelectOfScalar(_Select, Generic[_TSelect]): pass - else: from typing import GenericMeta # type: ignore diff --git a/sqlmodel/sql/expression.py.jinja2 b/sqlmodel/sql/expression.py.jinja2 index 9cd5d3f33e..033130393a 100644 --- a/sqlmodel/sql/expression.py.jinja2 +++ b/sqlmodel/sql/expression.py.jinja2 @@ -36,7 +36,6 @@ if sys.version_info.minor >= 7: class SelectOfScalar(_Select, Generic[_TSelect]): pass - else: from typing import GenericMeta # type: ignore From dbcaa50c698f0098d3c66262de1aef1514e4ae82 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 13 Dec 2021 10:41:14 +0000 Subject: [PATCH 011/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index c794f501d7..32a9bf9c4b 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🎨 Format `expression.py` and expression template, currently needed by CI. PR [#187](https://github.com/tiangolo/sqlmodel/pull/187) by [@tiangolo](https://github.com/tiangolo). * 🐛Fix docs light/dark theme switcher. PR [#1](https://github.com/tiangolo/sqlmodel/pull/1) by [@Lehoczky](https://github.com/Lehoczky). * 🔧 Add MkDocs Material social cards. PR [#90](https://github.com/tiangolo/sqlmodel/pull/90) by [@tiangolo](https://github.com/tiangolo). * ✨ Update type annotations and upgrade mypy. PR [#173](https://github.com/tiangolo/sqlmodel/pull/173) by [@tiangolo](https://github.com/tiangolo). From 14a9788eb1bf444b9d15f69c3504891e4099914d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 13 Dec 2021 11:47:07 +0100 Subject: [PATCH 012/906] =?UTF-8?q?=F0=9F=94=A7=20Split=20MkDocs=20insider?= =?UTF-8?q?s=20build=20in=20CI=20to=20support=20building=20from=20PRs=20(#?= =?UTF-8?q?186)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 4 ++++ mkdocs.insiders.yml | 4 ++++ mkdocs.yml | 3 --- 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 mkdocs.insiders.yml diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 31b799225c..72a79d19f7 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -56,7 +56,11 @@ jobs: key: mkdocs-cards-${{ github.ref }} path: .cache - name: Build Docs + if: github.event.pull_request.head.repo.fork == true run: python -m poetry run mkdocs build + - name: Build Docs with Insiders + if: github.event.pull_request.head.repo.fork == false + run: python -m poetry run mkdocs build --config-file mkdocs.insiders.yml - name: Zip docs run: python -m poetry run bash ./scripts/zip-docs.sh - uses: actions/upload-artifact@v2 diff --git a/mkdocs.insiders.yml b/mkdocs.insiders.yml new file mode 100644 index 0000000000..9f2775ff97 --- /dev/null +++ b/mkdocs.insiders.yml @@ -0,0 +1,4 @@ +INHERIT: mkdocs.yml +plugins: + - search + - social diff --git a/mkdocs.yml b/mkdocs.yml index e5c5cef342..6dfd51d057 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -30,9 +30,6 @@ edit_uri: '' google_analytics: - UA-205713594-2 - auto -plugins: -- search -- social nav: - SQLModel: index.md - features.md From 1c276ef88f4854a6105746abf822f134f59d33fb Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 13 Dec 2021 10:47:44 +0000 Subject: [PATCH 013/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 32a9bf9c4b..e920ec4e35 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Split MkDocs insiders build in CI to support building from PRs. PR [#186](https://github.com/tiangolo/sqlmodel/pull/186) by [@tiangolo](https://github.com/tiangolo). * 🎨 Format `expression.py` and expression template, currently needed by CI. PR [#187](https://github.com/tiangolo/sqlmodel/pull/187) by [@tiangolo](https://github.com/tiangolo). * 🐛Fix docs light/dark theme switcher. PR [#1](https://github.com/tiangolo/sqlmodel/pull/1) by [@Lehoczky](https://github.com/Lehoczky). * 🔧 Add MkDocs Material social cards. PR [#90](https://github.com/tiangolo/sqlmodel/pull/90) by [@tiangolo](https://github.com/tiangolo). From 580f3720596be7451638824f649ea0e5341f58f0 Mon Sep 17 00:00:00 2001 From: robcxyz <6512972+robcxyz@users.noreply.github.com> Date: Mon, 13 Dec 2021 04:30:20 -0700 Subject: [PATCH 014/906] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20Decim?= =?UTF-8?q?al=20fields=20from=20Pydantic=20and=20SQLAlchemy=20(#103)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/advanced/decimal.md | 148 ++++++++++++++++++ docs/advanced/index.md | 10 +- docs_src/advanced/__init__.py | 0 docs_src/advanced/decimal/__init__.py | 0 docs_src/advanced/decimal/tutorial001.py | 61 ++++++++ mkdocs.yml | 1 + sqlmodel/main.py | 5 +- tests/test_advanced/__init__.py | 0 tests/test_advanced/test_decimal/__init__.py | 0 .../test_decimal/test_tutorial001.py | 44 ++++++ 10 files changed, 262 insertions(+), 7 deletions(-) create mode 100644 docs/advanced/decimal.md create mode 100644 docs_src/advanced/__init__.py create mode 100644 docs_src/advanced/decimal/__init__.py create mode 100644 docs_src/advanced/decimal/tutorial001.py create mode 100644 tests/test_advanced/__init__.py create mode 100644 tests/test_advanced/test_decimal/__init__.py create mode 100644 tests/test_advanced/test_decimal/test_tutorial001.py diff --git a/docs/advanced/decimal.md b/docs/advanced/decimal.md new file mode 100644 index 0000000000..c0541b75df --- /dev/null +++ b/docs/advanced/decimal.md @@ -0,0 +1,148 @@ +# Decimal Numbers + +In some cases you might need to be able to store decimal numbers with guarantees about the precision. + +This is particularly important if you are storing things like **currencies**, **prices**, **accounts**, and others, as you would want to know that you wouldn't have rounding errors. + +As an example, if you open Python and sum `1.1` + `2.2` you would expect to see `3.3`, but you will actually get `3.3000000000000003`: + +```Python +>>> 1.1 + 2.2 +3.3000000000000003 +``` + +This is because of the way numbers are stored in "ones and zeros" (binary). But Python has a module and some types to have strict decimal values. You can read more about it in the official Python docs for Decimal. + +Because databases store data in the same ways as computers (in binary), they would have the same types of issues. And because of that, they also have a special **decimal** type. + +In most cases this would probably not be a problem, for example measuring views in a video, or the life bar in a videogame. But as you can imagine, this is particularly important when dealing with **money** and **finances**. + +## Decimal Types + +Pydantic has special support for `Decimal` types using the `condecimal()` special function. + +!!! tip + Pydantic 1.9, that will be released soon, has improved support for `Decimal` types, without needing to use the `condecimal()` function. + + But meanwhile, you can already use this feature with `condecimal()` in **SQLModel** it as it's explained here. + +When you use `condecimal()` you can specify the number of digits and decimal places to support. They will be validated by Pydantic (for example when using FastAPI) and the same information will also be used for the database columns. + +!!! info + For the database, **SQLModel** will use SQLAlchemy's `DECIMAL` type. + +## Decimals in SQLModel + +Let's say that each hero in the database will have an amount of money. We could make that field a `Decimal` type using the `condecimal()` function: + +```{.python .annotate hl_lines="12" } +{!./docs_src/advanced/decimal/tutorial001.py[ln:1-12]!} + +# More code here later 👇 +``` + +
+👀 Full file preview + +```Python +{!./docs_src/advanced/decimal/tutorial001.py!} +``` + +
+ +Here we are saying that `money` can have at most `5` digits with `max_digits`, **this includes the integers** (to the left of the decimal dot) **and the decimals** (to the right of the decimal dot). + +We are also saying that the number of decimal places (to the right of the decimal dot) is `3`, so we can have **3 decimal digits** for these numbers in the `money` field. This means that we will have **2 digits for the integer part** and **3 digits for the decimal part**. + +✅ So, for example, these are all valid numbers for the `money` field: + +* `12.345` +* `12.3` +* `12` +* `1.2` +* `0.123` +* `0` + +🚫 But these are all invalid numbers for that `money` field: + +* `1.2345` + * This number has more than 3 decimal places. +* `123.234` + * This number has more than 5 digits in total (integer and decimal part). +* `123` + * Even though this number doesn't have any decimals, we still have 3 places saved for them, which means that we can **only use 2 places** for the **integer part**, and this number has 3 integer digits. So, the allowed number of integer digits is `max_digits` - `decimal_places` = 2. + +!!! tip + Make sure you adjust the number of digits and decimal places for your own needs, in your own application. 🤓 + +## Create models with Decimals + +When creating new models you can actually pass normal (`float`) numbers, Pydantic will automatically convert them to `Decimal` types, and **SQLModel** will store them as `Decimal` types in the database (using SQLAlchemy). + +```Python hl_lines="4-6" +# Code above omitted 👆 + +{!./docs_src/advanced/decimal/tutorial001.py[ln:25-35]!} + +# Code below omitted 👇 +``` + +
+👀 Full file preview + +```Python +{!./docs_src/advanced/decimal/tutorial001.py!} +``` + +
+ +## Select Decimal data + +Then, when working with Decimal types, you can confirm that they indeed avoid those rounding errors from floats: + +```Python hl_lines="15-16" +# Code above omitted 👆 + +{!./docs_src/advanced/decimal/tutorial001.py[ln:38-51]!} + +# Code below omitted 👇 +``` + +
+👀 Full file preview + +```Python +{!./docs_src/advanced/decimal/tutorial001.py!} +``` + +
+ +## Review the results + +Now if you run this, instead of printing the unexpected number `3.3000000000000003`, it prints `3.300`: + +
+ +```console +$ python app.py + +// Some boilerplate and previous output omitted 😉 + +// The type of money is Decimal('1.100') +Hero 1: id=1 secret_name='Dive Wilson' age=None name='Deadpond' money=Decimal('1.100') + +// More output omitted here 🤓 + +// The type of money is Decimal('1.100') +Hero 2: id=3 secret_name='Tommy Sharp' age=48 name='Rusty-Man' money=Decimal('2.200') + +// No rounding errors, just 3.3! 🎉 +Total money: 3.300 +``` + +
+ +!!! warning + Although Decimal types are supported and used in the Python side, not all databases support it. In particular, SQLite doesn't support decimals, so it will convert them to the same floating `NUMERIC` type it supports. + + But decimals are supported by most of the other SQL databases. 🎉 diff --git a/docs/advanced/index.md b/docs/advanced/index.md index 588ac1d0e0..f6178249ce 100644 --- a/docs/advanced/index.md +++ b/docs/advanced/index.md @@ -1,12 +1,10 @@ # Advanced User Guide -The **Advanced User Guide** will be coming soon to a theater **documentation** near you... 😅 +The **Advanced User Guide** is gradually growing, you can already read about some advanced topics. -I just have to `add` it, `commit` it, and `refresh` it. 😉 +At some point it will include: -It will include: - -* How to use the `async` and `await` with the async session. +* How to use `async` and `await` with the async session. * How to run migrations. * How to combine **SQLModel** models with SQLAlchemy. -* ...and more. +* ...and more. 🤓 diff --git a/docs_src/advanced/__init__.py b/docs_src/advanced/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/advanced/decimal/__init__.py b/docs_src/advanced/decimal/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/advanced/decimal/tutorial001.py b/docs_src/advanced/decimal/tutorial001.py new file mode 100644 index 0000000000..fe5936f579 --- /dev/null +++ b/docs_src/advanced/decimal/tutorial001.py @@ -0,0 +1,61 @@ +from typing import Optional + +from pydantic import condecimal +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str + secret_name: str + age: Optional[int] = None + money: condecimal(max_digits=6, decimal_places=3) = Field(default=0) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson", money=1.1) + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador", money=0.001) + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48, money=2.2) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Deadpond") + results = session.exec(statement) + hero_1 = results.one() + print("Hero 1:", hero_1) + + statement = select(Hero).where(Hero.name == "Rusty-Man") + results = session.exec(statement) + hero_2 = results.one() + print("Hero 2:", hero_2) + + total_money = hero_1.money + hero_2.money + print(f"Total money: {total_money}") + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/mkdocs.yml b/mkdocs.yml index 6dfd51d057..41b44b6931 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -84,6 +84,7 @@ nav: - tutorial/fastapi/tests.md - Advanced User Guide: - advanced/index.md + - advanced/decimal.md - alternatives.md - help.md - contributing.md diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 84e26c4532..08eaf5956f 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -399,7 +399,10 @@ def get_sqlachemy_type(field: ModelField) -> Any: if issubclass(field.type_, bytes): return LargeBinary if issubclass(field.type_, Decimal): - return Numeric + return Numeric( + precision=getattr(field.type_, "max_digits", None), + scale=getattr(field.type_, "decimal_places", None), + ) if issubclass(field.type_, ipaddress.IPv4Address): return AutoString if issubclass(field.type_, ipaddress.IPv4Network): diff --git a/tests/test_advanced/__init__.py b/tests/test_advanced/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_advanced/test_decimal/__init__.py b/tests/test_advanced/test_decimal/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_advanced/test_decimal/test_tutorial001.py b/tests/test_advanced/test_decimal/test_tutorial001.py new file mode 100644 index 0000000000..1dafdfb322 --- /dev/null +++ b/tests/test_advanced/test_decimal/test_tutorial001.py @@ -0,0 +1,44 @@ +from decimal import Decimal +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function + +expected_calls = [ + [ + "Hero 1:", + { + "name": "Deadpond", + "age": None, + "id": 1, + "secret_name": "Dive Wilson", + "money": Decimal("1.100"), + }, + ], + [ + "Hero 2:", + { + "name": "Rusty-Man", + "age": 48, + "id": 3, + "secret_name": "Tommy Sharp", + "money": Decimal("2.200"), + }, + ], + ["Total money: 3.300"], +] + + +def test_tutorial(clear_sqlmodel): + from docs_src.advanced.decimal import tutorial001 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls From 75540f9728d4975918f032bf1c1a8750ab68d983 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 13 Dec 2021 11:30:57 +0000 Subject: [PATCH 015/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index e920ec4e35..6b6fb9a9cc 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Add support for Decimal fields from Pydantic and SQLAlchemy. PR [#103](https://github.com/tiangolo/sqlmodel/pull/103) by [@robcxyz](https://github.com/robcxyz). * 🔧 Split MkDocs insiders build in CI to support building from PRs. PR [#186](https://github.com/tiangolo/sqlmodel/pull/186) by [@tiangolo](https://github.com/tiangolo). * 🎨 Format `expression.py` and expression template, currently needed by CI. PR [#187](https://github.com/tiangolo/sqlmodel/pull/187) by [@tiangolo](https://github.com/tiangolo). * 🐛Fix docs light/dark theme switcher. PR [#1](https://github.com/tiangolo/sqlmodel/pull/1) by [@Lehoczky](https://github.com/Lehoczky). From 95c02962ba8ea1da2df4197f29166b889e2eb6c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 13 Dec 2021 12:37:59 +0100 Subject: [PATCH 016/906] =?UTF-8?q?=E2=9C=8F=20Update=20decimal=20tutorial?= =?UTF-8?q?=20source=20for=20consistency=20(#188)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs_src/advanced/decimal/tutorial001.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs_src/advanced/decimal/tutorial001.py b/docs_src/advanced/decimal/tutorial001.py index fe5936f579..1b16770cc6 100644 --- a/docs_src/advanced/decimal/tutorial001.py +++ b/docs_src/advanced/decimal/tutorial001.py @@ -9,7 +9,7 @@ class Hero(SQLModel, table=True): name: str secret_name: str age: Optional[int] = None - money: condecimal(max_digits=6, decimal_places=3) = Field(default=0) + money: condecimal(max_digits=5, decimal_places=3) = Field(default=0) sqlite_file_name = "database.db" From 7eadc905586c4c29189a9b99001b284bba0e1bbe Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 13 Dec 2021 11:38:40 +0000 Subject: [PATCH 017/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 6b6fb9a9cc..53e03362a3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Update decimal tutorial source for consistency. PR [#188](https://github.com/tiangolo/sqlmodel/pull/188) by [@tiangolo](https://github.com/tiangolo). * ✨ Add support for Decimal fields from Pydantic and SQLAlchemy. PR [#103](https://github.com/tiangolo/sqlmodel/pull/103) by [@robcxyz](https://github.com/robcxyz). * 🔧 Split MkDocs insiders build in CI to support building from PRs. PR [#186](https://github.com/tiangolo/sqlmodel/pull/186) by [@tiangolo](https://github.com/tiangolo). * 🎨 Format `expression.py` and expression template, currently needed by CI. PR [#187](https://github.com/tiangolo/sqlmodel/pull/187) by [@tiangolo](https://github.com/tiangolo). From 02697459b8ea25cee72e81239828139698a66ac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 13 Dec 2021 12:41:51 +0100 Subject: [PATCH 018/906] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.0.?= =?UTF-8?q?5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 13 ++++++++++++- sqlmodel/__init__.py | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 53e03362a3..e6cc558cf5 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,8 +2,19 @@ ## Latest Changes + +## 0.0.5 + +### Features + +* ✨ Add support for Decimal fields from Pydantic and SQLAlchemy. Original PR [#103](https://github.com/tiangolo/sqlmodel/pull/103) by [@robcxyz](https://github.com/robcxyz). New docs: [Advanced User Guide - Decimal Numbers](https://sqlmodel.tiangolo.com/advanced/decimal/). + +### Docs + * ✏ Update decimal tutorial source for consistency. PR [#188](https://github.com/tiangolo/sqlmodel/pull/188) by [@tiangolo](https://github.com/tiangolo). -* ✨ Add support for Decimal fields from Pydantic and SQLAlchemy. PR [#103](https://github.com/tiangolo/sqlmodel/pull/103) by [@robcxyz](https://github.com/robcxyz). + +### Internal + * 🔧 Split MkDocs insiders build in CI to support building from PRs. PR [#186](https://github.com/tiangolo/sqlmodel/pull/186) by [@tiangolo](https://github.com/tiangolo). * 🎨 Format `expression.py` and expression template, currently needed by CI. PR [#187](https://github.com/tiangolo/sqlmodel/pull/187) by [@tiangolo](https://github.com/tiangolo). * 🐛Fix docs light/dark theme switcher. PR [#1](https://github.com/tiangolo/sqlmodel/pull/1) by [@Lehoczky](https://github.com/Lehoczky). diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index bbe86ee2ae..78f47e21e7 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.0.4" +__version__ = "0.0.5" # Re-export from SQLAlchemy from sqlalchemy.engine import create_mock_engine as create_mock_engine From 32b5b39f2d2db8fff71a95a7719a2f2f89125d06 Mon Sep 17 00:00:00 2001 From: Sebastian Marines <18373185+sebastianmarines@users.noreply.github.com> Date: Tue, 14 Dec 2021 10:58:40 -0600 Subject: [PATCH 019/906] =?UTF-8?q?=E2=9C=8F=20Fix=20typos=20in=20`docs/tu?= =?UTF-8?q?torial/index.md`=20and=20`docs/databases.md`=20(#5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/databases.md | 2 +- docs/tutorial/index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/databases.md b/docs/databases.md index cb085c67d2..d07601907e 100644 --- a/docs/databases.md +++ b/docs/databases.md @@ -250,7 +250,7 @@ As these **primary key** IDs can uniquely identify each row on the table for tea table relationships -So, in the table for heroes, we use the `team_id` column to define a relationship to the *foreign* table for teams. Each value in the `team_id` column on the table with heroes will be the same value as the `id` column of one row in the table wiwth teams. +So, in the table for heroes, we use the `team_id` column to define a relationship to the *foreign* table for teams. Each value in the `team_id` column on the table with heroes will be the same value as the `id` column of one row in the table with teams. In the table for heroes we have a **primary key** that is the `id`. But we also have another column `team_id` that refers to a **key** in a **foreign** table. There's a technical term for that too, the `team_id` is a "**foreign key**". diff --git a/docs/tutorial/index.md b/docs/tutorial/index.md index 6deb258802..b45881138d 100644 --- a/docs/tutorial/index.md +++ b/docs/tutorial/index.md @@ -97,7 +97,7 @@ $ python3 --version // This is too old! 😱 Python 3.5.6 // Let's see if python3.10 is available -$ python3.10 --verson +$ python3.10 --version // Oh, no, this one is not available 😔 command not found: python3.10 $ python3.9 --version From 64d7f5335748d02c8ee86677fec9d16e8720e948 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 14 Dec 2021 16:59:16 +0000 Subject: [PATCH 020/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index e6cc558cf5..3204724890 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typos in `docs/tutorial/index.md` and `docs/databases.md`. PR [#5](https://github.com/tiangolo/sqlmodel/pull/5) by [@sebastianmarines](https://github.com/sebastianmarines). ## 0.0.5 From 50e62cdcd995fed3b211f822c29dfa11e145267d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leynier=20Guti=C3=A9rrez=20Gonz=C3=A1lez?= Date: Tue, 14 Dec 2021 12:11:39 -0500 Subject: [PATCH 021/906] =?UTF-8?q?=E2=9C=8F=20Fix=20typo=20in=20docs/tuto?= =?UTF-8?q?rial/automatic-id-none-refresh.md=20(#14)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tutorial/automatic-id-none-refresh.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/automatic-id-none-refresh.md b/docs/tutorial/automatic-id-none-refresh.md index 215735e387..ed767a2121 100644 --- a/docs/tutorial/automatic-id-none-refresh.md +++ b/docs/tutorial/automatic-id-none-refresh.md @@ -399,7 +399,7 @@ In this case, after committing the object to the database with the **session**, ## Print Data After Closing the Session -Now, as a fnal experiment, we can also print data after the **session** is closed. +Now, as a final experiment, we can also print data after the **session** is closed. There are no surprises here, it still works: From 6615b111d951780dc9d6414bebc0d3dda62a85db Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 14 Dec 2021 17:12:17 +0000 Subject: [PATCH 022/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 3204724890..a8cfe5a5bd 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typo in `docs/tutorial/automatic-id-none-refresh.md`. PR [#14](https://github.com/tiangolo/sqlmodel/pull/14) by [@leynier](https://github.com/leynier). * ✏ Fix typos in `docs/tutorial/index.md` and `docs/databases.md`. PR [#5](https://github.com/tiangolo/sqlmodel/pull/5) by [@sebastianmarines](https://github.com/sebastianmarines). ## 0.0.5 From 2013c69c4d8100109677a9b957285307b49152cb Mon Sep 17 00:00:00 2001 From: Evan Grim Date: Tue, 14 Dec 2021 10:17:10 -0700 Subject: [PATCH 023/906] =?UTF-8?q?=E2=9C=8F=20Fix=20multiple=20typos=20an?= =?UTF-8?q?d=20some=20rewording=20(#22)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/databases.md | 2 +- docs/tutorial/connect/create-connected-tables.md | 2 +- docs/tutorial/limit-and-offset.md | 10 +++++----- docs/tutorial/select.md | 6 +++--- docs/tutorial/update.md | 6 +++--- docs/tutorial/where.md | 2 +- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/databases.md b/docs/databases.md index d07601907e..e29c73e506 100644 --- a/docs/databases.md +++ b/docs/databases.md @@ -85,7 +85,7 @@ Some examples of databases that work like this could be **PostgreSQL**, **MySQL* ### Distributed servers -In some cases, the database could even be a group server applications running on different machines, working together and communicating between them to be more efficient and handle more data. +In some cases, the database could even be a group of server applications running on different machines, working together and communicating between them to be more efficient and handle more data. In this case, your code would talk to one or more of these server applications running on different machines. diff --git a/docs/tutorial/connect/create-connected-tables.md b/docs/tutorial/connect/create-connected-tables.md index 3485ccf087..90301ee41f 100644 --- a/docs/tutorial/connect/create-connected-tables.md +++ b/docs/tutorial/connect/create-connected-tables.md @@ -107,7 +107,7 @@ Most of that should look familiar: The column will be named `team_id`. It will be an integer, and it could be `NULL` in the database (or `None` in Python), becase there could be some heroes that don't belong to any team. -As we don't have to explicitly pass `team_id=None` when creating a hero, we add a default of `None` to the `Field()`. +We add a default of `None` to the `Field()` so we don't have to explicitly pass `team_id=None` when creating a hero. Now, here's the new part: diff --git a/docs/tutorial/limit-and-offset.md b/docs/tutorial/limit-and-offset.md index 0a964cc508..3fb001cf97 100644 --- a/docs/tutorial/limit-and-offset.md +++ b/docs/tutorial/limit-and-offset.md @@ -81,7 +81,7 @@ In this case, instead of getting all the 7 rows, we are limiting them to only ge table with first 3 rows selected -## Run the Program on the Comamnd Line +## Run the Program on the Command Line If we run it on the command line, it will output: @@ -153,7 +153,7 @@ Each of those methods applies the change in the internal special select statemen **Offset** means "skip this many rows", and as we want to skip the ones we already saw, the first three, we use `.offset(3)`. -## Run the Program with Offset on the Comamnd Line +## Run the Program with Offset on the Command Line Now we can run the program on the command line, and it will output: @@ -207,9 +207,9 @@ The database right now has **only 7 rows**, so this query can only get 1 row. But don't worry, the database won't throw an error trying to get 3 rows when there's only one (as would happen with a Python list). -The database knows that we want to **limit** the number of results, but it doesn't necessarily has to find those many results. +The database knows that we want to **limit** the number of results, but it doesn't necessarily have to find that many results. -## Run the Program with the Last Batch on the Comamnd Line +## Run the Program with the Last Batch on the Command Line And if we run it in the command line, it will output: @@ -271,7 +271,7 @@ Of course, you can also combine `.limit()` and `.offset()` with `.where()` and o -## Run the Program with Limit and Where on the Comamnd Line +## Run the Program with Limit and Where on the Command Line If we run it on the command line, it will find all the heroes in the database with an age above 32. That would normally be 4 heroes. diff --git a/docs/tutorial/select.md b/docs/tutorial/select.md index 5f917f69ad..b5a092224f 100644 --- a/docs/tutorial/select.md +++ b/docs/tutorial/select.md @@ -97,9 +97,9 @@ FROM hero That would end up in the same result. Although we won't use that for **SQLModel**. -### `SELECT` Less Columns +### `SELECT` Fewer Columns -We can also `SELECT` less columns, for example: +We can also `SELECT` fewer columns, for example: ```SQL SELECT id, name @@ -150,7 +150,7 @@ Another variation is that most of the SQL keywords like `SELECT` can also be wri This is the interesting part. The tables returned by SQL databases **don't have to exist** in the database as independent tables. 🧙 -For example, in our database, we only have one table that has all the columns, `id`, `name`, `secret_name`, `age`. And here we are getting a result table with less columns. +For example, in our database, we only have one table that has all the columns, `id`, `name`, `secret_name`, `age`. And here we are getting a result table with fewer columns. One of the main points of SQL is to be able to keep the data structured in different tables, without repeating data, etc, and then query the database in many ways and get many different tables as a result. diff --git a/docs/tutorial/update.md b/docs/tutorial/update.md index 3348615762..420616d78a 100644 --- a/docs/tutorial/update.md +++ b/docs/tutorial/update.md @@ -39,7 +39,7 @@ In a similar way to `SELECT` statements, the first part defines the columns to w And the second part, with the `WHERE`, defines to which rows it should apply that update. -In this case, as we only have one hero with the name `"Spider-Boy"`, it will only apply the udpate in that row. +In this case, as we only have one hero with the name `"Spider-Boy"`, it will only apply the update in that row. !!! info Notice that in the `UPDATE` the single equals sign (`=`) means **assignment**, setting a column to some value. @@ -70,7 +70,7 @@ After that update, the data in the table will look like this, with the new age f !!! tip - It will probably be more common to find the row to update by Id, for example: + It will probably be more common to find the row to update by `id`, for example: ```SQL UPDATE hero @@ -340,7 +340,7 @@ Now let's review all that code: The update process with **SQLModel** is more or less the same as with creating new objects, you add them to the session, and then commit them. -This also means that you can update several fields (atributes, columns) at once, and you can also update several objects (heroes) at once: +This also means that you can update several fields (attributes, columns) at once, and you can also update several objects (heroes) at once: ```{ .python .annotate hl_lines="15-17 19-21 23" } # Code above omitted 👆 diff --git a/docs/tutorial/where.md b/docs/tutorial/where.md index e52173128b..249ccd530c 100644 --- a/docs/tutorial/where.md +++ b/docs/tutorial/where.md @@ -271,7 +271,7 @@ In the example above we are using two equal signs (`==`). That's called the "**e !!! tip An **operator** is just a symbol that is put beside one value or in the middle of two values to do something with them. - `==` is called **equality** operator because it checks if two things are **equal**. + `==` is called the **equality** operator because it checks if two things are **equal**. When writing Python, if you write something using this equality operator (`==`) like: From ead1bdc5329905b10a069d128371b0b523b4f20a Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 14 Dec 2021 17:17:55 +0000 Subject: [PATCH 024/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index a8cfe5a5bd..b6190a65ef 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix multiple typos and some rewording. PR [#22](https://github.com/tiangolo/sqlmodel/pull/22) by [@egrim](https://github.com/egrim). * ✏ Fix typo in `docs/tutorial/automatic-id-none-refresh.md`. PR [#14](https://github.com/tiangolo/sqlmodel/pull/14) by [@leynier](https://github.com/leynier). * ✏ Fix typos in `docs/tutorial/index.md` and `docs/databases.md`. PR [#5](https://github.com/tiangolo/sqlmodel/pull/5) by [@sebastianmarines](https://github.com/sebastianmarines). From dc3acda4ed214e4553d8393f87cd4f8e3577c612 Mon Sep 17 00:00:00 2001 From: Alexandre Batisse Date: Tue, 14 Dec 2021 18:20:54 +0100 Subject: [PATCH 025/906] =?UTF-8?q?=E2=9C=8F=20Fix=20typos=20in=20docs=20t?= =?UTF-8?q?itles=20(#28)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tutorial/fastapi/limit-and-offset.md | 2 +- docs/tutorial/fastapi/teams.md | 2 +- docs/tutorial/where.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/tutorial/fastapi/limit-and-offset.md b/docs/tutorial/fastapi/limit-and-offset.md index 6df18f429a..57043ceaf7 100644 --- a/docs/tutorial/fastapi/limit-and-offset.md +++ b/docs/tutorial/fastapi/limit-and-offset.md @@ -1,4 +1,4 @@ -# Read Heroes with Limit and Offset wtih FastAPI +# Read Heroes with Limit and Offset with FastAPI When a client sends a request to get all the heroes, we have been returning them all. diff --git a/docs/tutorial/fastapi/teams.md b/docs/tutorial/fastapi/teams.md index f0bce4c839..9bc4af78cf 100644 --- a/docs/tutorial/fastapi/teams.md +++ b/docs/tutorial/fastapi/teams.md @@ -1,4 +1,4 @@ -# FastAPI Path Opeartions for Teams - Other Models +# FastAPI Path Operations for Teams - Other Models Let's now update the **FastAPI** application to handle data for teams. diff --git a/docs/tutorial/where.md b/docs/tutorial/where.md index 249ccd530c..fd807127cc 100644 --- a/docs/tutorial/where.md +++ b/docs/tutorial/where.md @@ -204,7 +204,7 @@ We care specially about the **select** statement: -## Filter Rows Using `WHERE` wtih **SQLModel** +## Filter Rows Using `WHERE` with **SQLModel** Now, the same way that we add `WHERE` to a SQL statement to filter rows, we can add a `.where()` to a **SQLModel** `select()` statment to filter rows, which will filter the objects returned: From a159f31945a1a7715b6cd85aa658c5ce38a2a3da Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 14 Dec 2021 17:21:35 +0000 Subject: [PATCH 026/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index b6190a65ef..2fb44dac71 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typos in docs titles. PR [#28](https://github.com/tiangolo/sqlmodel/pull/28) by [@Batalex](https://github.com/Batalex). * ✏ Fix multiple typos and some rewording. PR [#22](https://github.com/tiangolo/sqlmodel/pull/22) by [@egrim](https://github.com/egrim). * ✏ Fix typo in `docs/tutorial/automatic-id-none-refresh.md`. PR [#14](https://github.com/tiangolo/sqlmodel/pull/14) by [@leynier](https://github.com/leynier). * ✏ Fix typos in `docs/tutorial/index.md` and `docs/databases.md`. PR [#5](https://github.com/tiangolo/sqlmodel/pull/5) by [@sebastianmarines](https://github.com/sebastianmarines). From 6cf94a979798ef168d503a2f2f61273341ee98ba Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Tue, 14 Dec 2021 20:29:28 +0300 Subject: [PATCH 027/906] =?UTF-8?q?=F0=9F=93=9D=20Add=20links=20to=20the?= =?UTF-8?q?=20license=20file=20(#29)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- README.md | 2 +- docs/index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 09a2819a46..5a63c9da44 100644 --- a/README.md +++ b/README.md @@ -212,4 +212,4 @@ And at the same time, ✨ it is also a **Pydantic** model ✨. You can use inher ## License -This project is licensed under the terms of the MIT license. +This project is licensed under the terms of the [MIT license](https://github.com/tiangolo/sqlmodel/blob/main/LICENSE). diff --git a/docs/index.md b/docs/index.md index 09a2819a46..5a63c9da44 100644 --- a/docs/index.md +++ b/docs/index.md @@ -212,4 +212,4 @@ And at the same time, ✨ it is also a **Pydantic** model ✨. You can use inher ## License -This project is licensed under the terms of the MIT license. +This project is licensed under the terms of the [MIT license](https://github.com/tiangolo/sqlmodel/blob/main/LICENSE). From 1b99c3148feab25ff9a4163b57c2ee2da54340dc Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 14 Dec 2021 17:30:07 +0000 Subject: [PATCH 028/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 2fb44dac71..6142d764e0 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Add links to the license file. PR [#29](https://github.com/tiangolo/sqlmodel/pull/29) by [@sobolevn](https://github.com/sobolevn). * ✏ Fix typos in docs titles. PR [#28](https://github.com/tiangolo/sqlmodel/pull/28) by [@Batalex](https://github.com/Batalex). * ✏ Fix multiple typos and some rewording. PR [#22](https://github.com/tiangolo/sqlmodel/pull/22) by [@egrim](https://github.com/egrim). * ✏ Fix typo in `docs/tutorial/automatic-id-none-refresh.md`. PR [#14](https://github.com/tiangolo/sqlmodel/pull/14) by [@leynier](https://github.com/leynier). From 410d7af6b654b9c42a0905c0ff7b0c60f0610bfe Mon Sep 17 00:00:00 2001 From: Yaqueline Hoyos Date: Tue, 14 Dec 2021 13:25:06 -0500 Subject: [PATCH 029/906] =?UTF-8?q?=E2=9C=8F=20Fix=20typo=20in=20FastAPI?= =?UTF-8?q?=20tutorial=20(#192)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tutorial/fastapi/simple-hero-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/fastapi/simple-hero-api.md b/docs/tutorial/fastapi/simple-hero-api.md index 8759bce2c2..8676136a46 100644 --- a/docs/tutorial/fastapi/simple-hero-api.md +++ b/docs/tutorial/fastapi/simple-hero-api.md @@ -152,7 +152,7 @@ It will be called when a user sends a request with a `POST` **operation** to the ## The **SQLModel** Advantage -Here's where having our **SQLModel** class models be both **SQLAlchemy** models and **Pydantic** models at the same tieme shine. ✨ +Here's where having our **SQLModel** class models be both **SQLAlchemy** models and **Pydantic** models at the same time shine. ✨ Here we use the **same** class model to define the **request body** that will be received by our API. From 3d7b74746cad55b5c1c991408ab5e234bd095a1b Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 14 Dec 2021 18:25:46 +0000 Subject: [PATCH 030/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 6142d764e0..f26c10c169 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typo in FastAPI tutorial. PR [#192](https://github.com/tiangolo/sqlmodel/pull/192) by [@yaquelinehoyos](https://github.com/yaquelinehoyos). * 📝 Add links to the license file. PR [#29](https://github.com/tiangolo/sqlmodel/pull/29) by [@sobolevn](https://github.com/sobolevn). * ✏ Fix typos in docs titles. PR [#28](https://github.com/tiangolo/sqlmodel/pull/28) by [@Batalex](https://github.com/Batalex). * ✏ Fix multiple typos and some rewording. PR [#22](https://github.com/tiangolo/sqlmodel/pull/22) by [@egrim](https://github.com/egrim). From 155c6178cd18671c8046c27c22ed580a9e4b1fb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 28 Dec 2021 11:48:03 +0100 Subject: [PATCH 031/906] =?UTF-8?q?=E2=9C=A8=20Document=20indexes=20and=20?= =?UTF-8?q?make=20them=20opt-in=20(#205)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../img/tutorial/indexes/dictionary001.drawio | 97 +++++ docs/img/tutorial/indexes/dictionary001.svg | 57 +++ .../img/tutorial/indexes/dictionary002.drawio | 97 +++++ docs/img/tutorial/indexes/dictionary002.svg | 1 + .../img/tutorial/indexes/dictionary003.drawio | 97 +++++ docs/img/tutorial/indexes/dictionary003.svg | 1 + .../img/tutorial/indexes/dictionary004.drawio | 100 +++++ docs/img/tutorial/indexes/dictionary004.svg | 1 + .../img/tutorial/indexes/dictionary005.drawio | 97 +++++ docs/img/tutorial/indexes/dictionary005.svg | 1 + .../img/tutorial/indexes/dictionary006.drawio | 100 +++++ docs/img/tutorial/indexes/dictionary006.svg | 1 + .../img/tutorial/indexes/dictionary007.drawio | 100 +++++ docs/img/tutorial/indexes/dictionary007.svg | 1 + .../img/tutorial/indexes/dictionary008.drawio | 103 +++++ docs/img/tutorial/indexes/dictionary008.svg | 1 + docs/img/tutorial/indexes/techbook001.drawio | 92 ++++ docs/img/tutorial/indexes/techbook001.svg | 1 + .../connect/create-connected-tables.md | 1 + docs/tutorial/fastapi/multiple-models.md | 25 ++ docs/tutorial/indexes.md | 406 ++++++++++++++++++ docs/tutorial/one.md | 6 +- docs/tutorial/update.md | 2 +- docs/tutorial/where.md | 8 +- docs_src/advanced/decimal/tutorial001.py | 4 +- .../code_structure/tutorial001/models.py | 6 +- .../code_structure/tutorial002/hero_model.py | 4 +- .../code_structure/tutorial002/team_model.py | 2 +- .../connect/create_tables/tutorial001.py | 6 +- .../tutorial/connect/delete/tutorial001.py | 6 +- .../tutorial/connect/insert/tutorial001.py | 6 +- .../tutorial/connect/select/tutorial001.py | 6 +- .../tutorial/connect/select/tutorial002.py | 6 +- .../tutorial/connect/select/tutorial003.py | 6 +- .../tutorial/connect/select/tutorial004.py | 6 +- .../tutorial/connect/select/tutorial005.py | 6 +- .../tutorial/connect/update/tutorial001.py | 6 +- docs_src/tutorial/delete/tutorial001.py | 4 +- docs_src/tutorial/delete/tutorial002.py | 4 +- .../fastapi/app_testing/tutorial001/main.py | 4 +- .../tutorial/fastapi/delete/tutorial001.py | 4 +- .../fastapi/limit_and_offset/tutorial001.py | 4 +- .../fastapi/multiple_models/tutorial001.py | 4 +- .../fastapi/multiple_models/tutorial002.py | 4 +- .../tutorial/fastapi/read_one/tutorial001.py | 4 +- .../fastapi/relationships/tutorial001.py | 6 +- .../fastapi/response_model/tutorial001.py | 4 +- .../session_with_dependency/tutorial001.py | 4 +- .../fastapi/simple_hero_api/tutorial001.py | 4 +- .../tutorial/fastapi/teams/tutorial001.py | 6 +- .../tutorial/fastapi/update/tutorial001.py | 4 +- docs_src/tutorial/indexes/__init__.py | 0 docs_src/tutorial/indexes/tutorial001.py | 51 +++ docs_src/tutorial/indexes/tutorial002.py | 59 +++ docs_src/tutorial/many_to_many/tutorial001.py | 6 +- docs_src/tutorial/many_to_many/tutorial002.py | 6 +- docs_src/tutorial/many_to_many/tutorial003.py | 6 +- .../tutorial/offset_and_limit/tutorial001.py | 4 +- .../tutorial/offset_and_limit/tutorial002.py | 4 +- .../tutorial/offset_and_limit/tutorial003.py | 4 +- .../tutorial/offset_and_limit/tutorial004.py | 4 +- docs_src/tutorial/one/tutorial001.py | 4 +- docs_src/tutorial/one/tutorial002.py | 4 +- docs_src/tutorial/one/tutorial003.py | 4 +- docs_src/tutorial/one/tutorial004.py | 4 +- docs_src/tutorial/one/tutorial005.py | 4 +- docs_src/tutorial/one/tutorial006.py | 4 +- docs_src/tutorial/one/tutorial007.py | 4 +- docs_src/tutorial/one/tutorial008.py | 4 +- docs_src/tutorial/one/tutorial009.py | 4 +- .../back_populates/tutorial001.py | 6 +- .../back_populates/tutorial002.py | 6 +- .../back_populates/tutorial003.py | 10 +- .../tutorial001.py | 6 +- .../tutorial001.py | 6 +- .../read_relationships/tutorial001.py | 6 +- .../read_relationships/tutorial002.py | 6 +- docs_src/tutorial/update/tutorial001.py | 4 +- docs_src/tutorial/update/tutorial002.py | 4 +- docs_src/tutorial/update/tutorial003.py | 4 +- docs_src/tutorial/update/tutorial004.py | 4 +- mkdocs.yml | 5 +- sqlmodel/main.py | 2 +- .../test_multiple_models/test_tutorial001.py | 15 + .../test_multiple_models/test_tutorial002.py | 15 + tests/test_tutorial/test_indexes/__init__.py | 0 .../test_indexes/test_tutorial001.py | 35 ++ .../test_indexes/test_tutorial006.py | 36 ++ .../test_where/test_tutorial003.py | 7 +- .../test_where/test_tutorial004.py | 7 +- .../test_where/test_tutorial011.py | 7 +- 91 files changed, 1755 insertions(+), 142 deletions(-) create mode 100644 docs/img/tutorial/indexes/dictionary001.drawio create mode 100644 docs/img/tutorial/indexes/dictionary001.svg create mode 100644 docs/img/tutorial/indexes/dictionary002.drawio create mode 100644 docs/img/tutorial/indexes/dictionary002.svg create mode 100644 docs/img/tutorial/indexes/dictionary003.drawio create mode 100644 docs/img/tutorial/indexes/dictionary003.svg create mode 100644 docs/img/tutorial/indexes/dictionary004.drawio create mode 100644 docs/img/tutorial/indexes/dictionary004.svg create mode 100644 docs/img/tutorial/indexes/dictionary005.drawio create mode 100644 docs/img/tutorial/indexes/dictionary005.svg create mode 100644 docs/img/tutorial/indexes/dictionary006.drawio create mode 100644 docs/img/tutorial/indexes/dictionary006.svg create mode 100644 docs/img/tutorial/indexes/dictionary007.drawio create mode 100644 docs/img/tutorial/indexes/dictionary007.svg create mode 100644 docs/img/tutorial/indexes/dictionary008.drawio create mode 100644 docs/img/tutorial/indexes/dictionary008.svg create mode 100644 docs/img/tutorial/indexes/techbook001.drawio create mode 100644 docs/img/tutorial/indexes/techbook001.svg create mode 100644 docs/tutorial/indexes.md create mode 100644 docs_src/tutorial/indexes/__init__.py create mode 100644 docs_src/tutorial/indexes/tutorial001.py create mode 100644 docs_src/tutorial/indexes/tutorial002.py create mode 100644 tests/test_tutorial/test_indexes/__init__.py create mode 100644 tests/test_tutorial/test_indexes/test_tutorial001.py create mode 100644 tests/test_tutorial/test_indexes/test_tutorial006.py diff --git a/docs/img/tutorial/indexes/dictionary001.drawio b/docs/img/tutorial/indexes/dictionary001.drawio new file mode 100644 index 0000000000..659f6b52a4 --- /dev/null +++ b/docs/img/tutorial/indexes/dictionary001.drawio @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/img/tutorial/indexes/dictionary001.svg b/docs/img/tutorial/indexes/dictionary001.svg new file mode 100644 index 0000000000..b543793a25 --- /dev/null +++ b/docs/img/tutorial/indexes/dictionary001.svg @@ -0,0 +1,57 @@ +
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
M
M
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/img/tutorial/indexes/dictionary002.drawio b/docs/img/tutorial/indexes/dictionary002.drawio new file mode 100644 index 0000000000..cb1857b1ad --- /dev/null +++ b/docs/img/tutorial/indexes/dictionary002.drawio @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/img/tutorial/indexes/dictionary002.svg b/docs/img/tutorial/indexes/dictionary002.svg new file mode 100644 index 0000000000..677687d248 --- /dev/null +++ b/docs/img/tutorial/indexes/dictionary002.svg @@ -0,0 +1 @@ +
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
M
M
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/img/tutorial/indexes/dictionary003.drawio b/docs/img/tutorial/indexes/dictionary003.drawio new file mode 100644 index 0000000000..845eb065cd --- /dev/null +++ b/docs/img/tutorial/indexes/dictionary003.drawio @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/img/tutorial/indexes/dictionary003.svg b/docs/img/tutorial/indexes/dictionary003.svg new file mode 100644 index 0000000000..d667a68893 --- /dev/null +++ b/docs/img/tutorial/indexes/dictionary003.svg @@ -0,0 +1 @@ +
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/img/tutorial/indexes/dictionary004.drawio b/docs/img/tutorial/indexes/dictionary004.drawio new file mode 100644 index 0000000000..14bbb1e26e --- /dev/null +++ b/docs/img/tutorial/indexes/dictionary004.drawio @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/img/tutorial/indexes/dictionary004.svg b/docs/img/tutorial/indexes/dictionary004.svg new file mode 100644 index 0000000000..f881d6c9c2 --- /dev/null +++ b/docs/img/tutorial/indexes/dictionary004.svg @@ -0,0 +1 @@ +
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
F
F
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/img/tutorial/indexes/dictionary005.drawio b/docs/img/tutorial/indexes/dictionary005.drawio new file mode 100644 index 0000000000..9e339c177e --- /dev/null +++ b/docs/img/tutorial/indexes/dictionary005.drawio @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/img/tutorial/indexes/dictionary005.svg b/docs/img/tutorial/indexes/dictionary005.svg new file mode 100644 index 0000000000..9d376245c0 --- /dev/null +++ b/docs/img/tutorial/indexes/dictionary005.svg @@ -0,0 +1 @@ +
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/img/tutorial/indexes/dictionary006.drawio b/docs/img/tutorial/indexes/dictionary006.drawio new file mode 100644 index 0000000000..3c669d323f --- /dev/null +++ b/docs/img/tutorial/indexes/dictionary006.drawio @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/img/tutorial/indexes/dictionary006.svg b/docs/img/tutorial/indexes/dictionary006.svg new file mode 100644 index 0000000000..30be80ea8b --- /dev/null +++ b/docs/img/tutorial/indexes/dictionary006.svg @@ -0,0 +1 @@ +
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
C
C
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/img/tutorial/indexes/dictionary007.drawio b/docs/img/tutorial/indexes/dictionary007.drawio new file mode 100644 index 0000000000..89b32cabaf --- /dev/null +++ b/docs/img/tutorial/indexes/dictionary007.drawio @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/img/tutorial/indexes/dictionary007.svg b/docs/img/tutorial/indexes/dictionary007.svg new file mode 100644 index 0000000000..79e795060e --- /dev/null +++ b/docs/img/tutorial/indexes/dictionary007.svg @@ -0,0 +1 @@ +
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/img/tutorial/indexes/dictionary008.drawio b/docs/img/tutorial/indexes/dictionary008.drawio new file mode 100644 index 0000000000..ac08ad04d4 --- /dev/null +++ b/docs/img/tutorial/indexes/dictionary008.drawio @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/img/tutorial/indexes/dictionary008.svg b/docs/img/tutorial/indexes/dictionary008.svg new file mode 100644 index 0000000000..013a4d64a3 --- /dev/null +++ b/docs/img/tutorial/indexes/dictionary008.svg @@ -0,0 +1 @@ +
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
D
D
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/img/tutorial/indexes/techbook001.drawio b/docs/img/tutorial/indexes/techbook001.drawio new file mode 100644 index 0000000000..de1c25668c --- /dev/null +++ b/docs/img/tutorial/indexes/techbook001.drawio @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/img/tutorial/indexes/techbook001.svg b/docs/img/tutorial/indexes/techbook001.svg new file mode 100644 index 0000000000..8b0c09ddcf --- /dev/null +++ b/docs/img/tutorial/indexes/techbook001.svg @@ -0,0 +1 @@ +
Technical Book
Technical Book
Chapter 1
Chapter 1
Chapter 2
Chapter 2
Chapter 3
Chapter 3
Chapter 4
Chapter 4
Chapter 5
Chapter 5
Chapter 6
Chapter 6
Chapter 7
Chapter 7
Book Index
Book Index
Database
Database
Python
Python
Files
Files
Editors
Editors
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/tutorial/connect/create-connected-tables.md b/docs/tutorial/connect/create-connected-tables.md index 90301ee41f..452c904ebe 100644 --- a/docs/tutorial/connect/create-connected-tables.md +++ b/docs/tutorial/connect/create-connected-tables.md @@ -78,6 +78,7 @@ The `Team` model will be in a table automatically named `"team"`, and it will ha * `id`, the primary key, automatically generated by the database * `name`, the name of the team + * We also tell **SQLModel** to create an index for this column * `headquarters`, the headquarters of the team And finally we mark it as a table in the config. diff --git a/docs/tutorial/fastapi/multiple-models.md b/docs/tutorial/fastapi/multiple-models.md index 5cac6dda91..d313874c98 100644 --- a/docs/tutorial/fastapi/multiple-models.md +++ b/docs/tutorial/fastapi/multiple-models.md @@ -305,6 +305,31 @@ And of course, all these fields will be in the columns for the resulting `hero` And those inherited fields will also be in the **autocompletion** and **inline errors** in editors, etc. +### Columns and Inheritance with Multiple Models + +Notice that the parent model `HeroBase` is not a **table model**, but still, we can declare `name` and `age` using `Field(index=True)`. + +```Python hl_lines="4 6 9" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py[ln:7-14]!} + +# Code below omitted 👇 +``` + +
+👀 Full file preview + +```Python +{!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py!} +``` + +
+ +This won't affect this parent **data model** `HeroBase`. + +But once the child model `Hero` (the actual **table model**) inherits those fields, it will use those field configurations to create the indexes when creating the tables in the database. + ### The `HeroCreate` **Data Model** Now let's see the `HeroCreate` model that will be used to define the data that we want to receive in the API when creating a new hero. diff --git a/docs/tutorial/indexes.md b/docs/tutorial/indexes.md new file mode 100644 index 0000000000..6513d7d462 --- /dev/null +++ b/docs/tutorial/indexes.md @@ -0,0 +1,406 @@ +# Indexes - Optimize Queries + +We just saw how to get some data `WHERE` a **condition** is true. For example, where the hero **name is "Deadpond"**. + +If we just create the tables and the data as we have been doing, when we `SELECT` some data using `WHERE`, the database would have to **scan** through **each one of the records** to find the ones that **match**. This is not a problem with 3 heroes as in these examples. + +But imagine that your database has **thousands** or **millions** of **records**, if every time you want to find the heroes with the name "Deadpond" it has to scan through **all** of the records to find all the possible matches, then that becomes problematic, as it would be too slow. + +I'll show you how to handle it with a database **index**. + +The change in the code is **extremely small**, but it's useful to understand what's happening behind the scenes, so I'll show you **how it all works** and what it means. + +--- + +If you already executed the previous examples and have a database with data, **remove the database file** before running each example, that way you won't have duplicate data and you will be able to get the same results. + +## No Time to Explain + +Are you already a **SQL expert** and don't have time for all my explanations? + +Fine, in that case, you can **sneak peek** the final code to create indexes here. + +
+👀 Full file preview + +```Python hl_lines="8 10" +{!./docs_src/tutorial/indexes/tutorial002.py!} +``` + +
+ +..but if you are not an expert, **continue reading**, this will probably be useful. 🤓 + +## What is an Index + +In general, an **index** is just something we can have to help us **find things faster**. It normally works by having things in **order**. Let's think about some real-life examples before even thinking about databases and code. + +### An Index and a Dictionary + +Imagine a **dictionary**, a book with definitions of words. 📔 ...not a Python `dict`. 😅 + +Let's say that you want to **find a word**, for example the word "**database**". You take the dictionary, and open it somewhere, for example in the middle. Maybe you see some definitions of words that start with `m`, like `manual`, so you conclude that you are in the letter `m` in the dictionary. + + + +You know that in the alphabet, the letter `d` for `database` comes **before** the letter `m` for `manual`. + + + +So, you know you have to search in the dictionary **before** the point you currently are. You still don't know where the word `database` is, because you don't know exactly where the letter `d` is in the dictionary, but you know that **it is not after** that point, you can now **discard the right half** of the dictionary in your search. + + + +Next, you **open the dictionary again**, but only taking into account the **half of the dictionary** that can contain the word you want, the **left part of the dictionary**. You open it in the middle of that left part and now you arrive maybe at the letter `f`. + + + +You know that `d` from `database` comes before `f`. So it has to be **before** that. But now you know that `database` **is not after** that point, and you can discard the dictionary from that point onward. + + + +Now you have a **small section of dictionary** to search (only a **quarter** of dictionary can have your word). You take that **quarter** of the pages at the start of the dictionary that can contain your word, and open it in the middle of that section. Maybe you arrive at the letter `c`. + + + +You know the word `database` has to be **after** that and **not before** that point, so you can discard the left part of that block of pages. + + + +You repeat this process **a few more times**, and you finally arrive at the letter `d`, you continue with the same process in that section for the letter `d` and you finally **find the word** `database`. 🎉 + + + +You had to open the dictionary a few times, maybe **5 or 10**. That's actually **very little work** compared to what it could have been. + +!!! note "Technical Details" + Do you like **fancy words**? Cool! Programmers tend to like fancy words. 😅 + + That algorithm I showed you above is called **Binary Search**. + + It's called like that because you **search** something by splitting the dictionary (or any ordered list of things) in **two** ("binary" means "two") parts. And you do that process multiple times until you find what you want. + +### An Index and a Novel + +Let's now imagine you are reading a **novel book**. And someone told you that at some point, they mention a **database**, and you want to find that chapter. + +How do you find the word "*database*" there? You might have to read **the entire book** to find where the word "*database*" is located in the book. So, instead of opening the book 5 or 10 times, you would have to open each of the **500 pages** and read them one by one until you find the word. You might enjoy the book, though. 😅 + +But if we are only interested in **quickly finding information** (as when working with SQL databases), then reading each of the 500 pages is **too inefficient** when there could be an option to open the book in 5 or 10 places and find what you're looking for. + +### A Technical Book with an Index + +Now let's imagine you are reading a technical book. For example, with several topics about programming. And there's a couple of sections where it talks about a **database**. + +This book might have a **book index**: a section in the book that has some **names of topics covered** and the **page numbers** in the book where you can read about them. And the topic names are **sorted** in alphabetic order, pretty much like a dictionary (a book with words, as in the previous example). + +In this case, you can open that book in the end (or in the beginning) to find the **book index** section, it would have only a few pages. And then, you can do the same process as with the **dictionary** example above. + +Open the index, and after **5 or 10 steps**, quickly find the topic "**database**" with the page numbers where that is covered, for example "page 253 in Chapter 5". Now you used the dictionary technique to find the **topic**, and that topic gave you a **page number**. + +Now you know that you need to find "**page 253**". But by looking at the closed book you still don't know where that page is, so you have to **find that page**. To find it, you can do the same process again, but this time, instead of searching for a **topic** in the **index**, you are searching for a **page number** in the **entire book**. And after **5 or 10 more steps**, you find the page 253 in Chapter 5. + + + +After this, even though this book is not a dictionary and has some particular content, you were able to **find the section** in the book that talks about a "**database**" in a **few steps** (say 10 or 20, instead of reading all the 500 pages). + +The main point is that the index is **sorted**, so we can use the same process we used for the **dictionary** to find the topic. And then that gives us a page number, and the **page numbers are also sorted**! 😅 + +When we have a list of sorted things we can apply the same technique, and that's the whole trick here, we use the same technique first for the **topics** in the index and then for the **page numbers** to find the actual chapter. + +Such efficiency! 😎 + +## What are Database Indexes + +**Database indexes** are very similar to **book indexes**. + +Database indexes store some info, some keys, in a way that makes it **easy and fast to find** (for example sorted), and then for each key they **point to some data somewhere else** in the database. + +Let's see a more clear example. Let's say you have this table in a database: + + + + + + + + + + + + + + +
idnamesecret_nameage
1DeadpondDive Wilsonnull
2Spider-BoyPedro Parqueadornull
3Rusty-ManTommy Sharp48
+ +And let's imagine you have **many more rows**, many more heroes. Probably **thousands**. + +If you tell the SQL database to get you a hero by a specific name, for example `Spider-Boy` (by using the `name` in the `WHERE` part of the SQL query), the database will have to **scan** all the heroes, checking **one by one** to find all the ones with a name of `Spider-Boy`. + +In this case, there's only one, but there's nothing limiting the database from having **more records with the same name**. And because of that, the database would **continue searching** and checking each one of the records, which would be very slow. + +But now let's say that the database has an index for the column `name`. The index could look something like this, we could imagine that the index is like an additional special table that the database manages automatically: + + + + + + + + + + + + + + +
nameid
Deadpond1
Rusty-Man3
Spider-Boy2
+ +It would have each `name` field from the `hero` table **in order**. It would not be sorted by `id`, but by `name` (in alphabetical order, as the `name` is a string). So, first it would have `Deadpond`, then `Rusty-Man`, and last `Spider-Boy`. It would also include the `id` of each hero. Remember that this could have **thousands** of heroes. + +Then the database would be able to use more or less the same ideas in the examples above with the **dictionary** and the **book index**. + +It could start somewhere (for example, in the middle of the index). It could arrive at some hero there in the middle, like `Rusty-Man`. And because the **index** has the `name` fields in order, the database would know that it can **discard all the previous index rows** and **only search** in the following index rows. + + + + + + + + + + + + + + +
nameid
Deadpond1
Rusty-Man3
Spider-Boy2
+ +And that way, as with the example with the dictionary above, **instead of reading thousands of heroes**, the database would be able to do a few steps, say **5 or 10 steps**, and arrive at the row of the index that has `Spider-Boy`, even if the table (and index) has thousands of rows: + + + + + + + + + + + + + + +
nameid
Deadpond1
Rusty-Man3
✨ Spider-Boy ✨2
+ +Then by looking at **this index row**, it would know that the `id` for `Spider-Boy` in the `hero` table is `2`. + +So then it could **search that `id`** in the `hero` table using more or less the **same technique**. + +That way, in the end, instead of reading thousands of records, the database only had to do **a few steps** to find the hero we wanted. + +## Updating the Index + +As you can imagine, for all this to work, the index would need to be **up to date** with the data in the database. + +If you had to update it **manually** in code, it would be very cumbersome and **error-prone**, as it would be easy to end up in a state where the index is not up to date and points to incorrect data. 😱 + +Here's the good news: when you create an index in a **SQL Database**, the database takes care of **updating** it **automatically** whenever it's necessary. 😎🎉 + +If you **add new records** to the `hero` table, the database will **automatically** update the index. It will do the **same process** of **finding** the right place to put the new index data (those **5 or 10 steps** described above), and then it will save the new index information there. The same would happen when you **update** or **delete** data. + +Defining and creating an index is very **easy** with SQL databases. And then **using it** is even easier... it's transparent. The database will figure out which index to use automatically, the SQL queries don't even change. + +So, in SQL databases **indexes are great**! And are super **easy to use**. Why not just have indexes for everything? .....Because indexes also have a "**cost**" in computation and storage (disk space). + +## Index Cost + +There's a **cost** associated with **indexes**. 💰 + +When you don't have an index and add a **new row** to the table `hero`, the database has to perform **1 operation** to add the new hero row at the end of the table. + +But if you have an **index** for the **hero names**, now the database has to perform the same **1 operation** to add that row **plus** some extra **5 or 10 operations** in the index, to find the right spot for the name, to then add that **index record** there. + +And if you have an index for the `name`, one for the `age`, and one for the `secret_name`, now the database has to perform the same **1 operation** to add that row **plus** some extra **5 or 10 operations** in the index **times 3**, for each of the indexes. This means that now adding one row takes something like **31 operations**. + +This also means that you are **exchanging** the time it takes to **read** data for the time it takes to **write** data plus some extra **space** in the database. + +If you have queries that get data out of the database comparing each one of those fields (for example using `WHERE`), then it makes total sense to have indexes for each one of them. Because **31 operations** while creating or updating data (plus the space of the index) is much, much better than the possible **500 or 1000 operations** to read all the rows to be able to compare them using each field. + +But if you **never** have queries that find records by the `secret_name` (you never use `secret_name` in the `WHERE` part) it probably doesn't make sense to have an index for the `secret_name` field/column, as that will increase the computational and space **cost** of writing and updating the database. + +## Create an Index with SQL + +Phew, that was a lot of theory and explanations. 😅 + +The most important thing about indexes is **understanding** them, how, and when to use them. + +Let's now see the **SQL** syntax to create an **index**. It is very simple: + +```SQL hl_lines="3" +CREATE INDEX ix_hero_name +ON hero (name) +``` + +This means, more or less: + +> Hey SQL database 👋, please `CREATE` an `INDEX` for me. +> +> I want the name of the index to be `ix_hero_name`. +> +> This index should be `ON` the table `hero`, it refers to that table. +> +> The column I want you to use for it is `name`. + +## Declare Indexes with SQLModel + +And now let's see how to define indexes in **SQLModel**. + +The change in code is underwhelming, it's very simple. 😆 + +Here's the `Hero` model we had before: + +```Python hl_lines="8" +{!./docs_src/tutorial/where/tutorial001.py[ln:1-10]!} + +# Code below omitted 👇 +``` + +
+👀 Full file preview + +```Python +{!./docs_src/tutorial/where/tutorial001.py!} +``` + +
+ +Let's now update it to tell **SQLModel** to create an index for the `name` field when creating the table: + +```Python hl_lines="8" +{!./docs_src/tutorial/indexes/tutorial001.py[ln:1-10]!} + +# Code below omitted 👇 +``` + +
+👀 Full file preview + +```Python +{!./docs_src/tutorial/indexes/tutorial001.py!} +``` + +
+ +We use the same `Field()` again as we did before, and set `index=True`. That's it! 🚀 + +Notice that we didn't set an argument of `default=None` or anything similar. This means that **SQLModel** (thanks to Pydantic) will keep it as a **required** field. + +!!! info + SQLModel (actually SQLAlchemy) will **automatically generate the index name** for you. + + In this case the generated name would be `ix_hero_name`. + +## Query Data + +Now, to query the data using the field `name` and the new index we don't have to do anything special or different in the code, it's just **the same code**. + +The SQL database will figure it out **automatically**. ✨ + +This is great because it means that indexes are very **simple to use**. But it might also feel counterintuitive at first, as you are **not doing anything** explicitly in the code to make it obvious that the index is useful, it all happens in the database behind the scenes. + +```Python hl_lines="5" +# Code above omitted 👆 + +{!./docs_src/tutorial/indexes/tutorial001.py[ln:36-41]!} + +# Code below omitted 👇 +``` + +
+👀 Full file preview + +```Python +{!./docs_src/tutorial/indexes/tutorial001.py!} +``` + +
+ +This is exactly the same code as we had before, but now the database will **use the index** underneath. + +## Run the Program + +If you run the program now, you will see an output like this: + +
+ +```console +$ python app.py + +// Some boilerplate output omitted 😉 + +// Create the table +CREATE TABLE hero ( + id INTEGER, + name VARCHAR NOT NULL, + secret_name VARCHAR NOT NULL, + age INTEGER, + PRIMARY KEY (id) +) + +// Create the index 🤓🎉 +CREATE INDEX ix_hero_name ON hero (name) + +// The SELECT with WHERE looks the same +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero +WHERE hero.name = ? +INFO Engine [no key 0.00014s] ('Deadpond',) + +// The resulting hero +secret_name='Dive Wilson' age=None id=1 name='Deadpond' +``` + +
+ +## More Indexes + +We are going to query the `hero` table doing comparisons on the `age` field too, so we should **define an index** for that one as well: + +```Python hl_lines="10" +{!./docs_src/tutorial/indexes/tutorial002.py[ln:1-10]!} + +# Code below omitted 👇 +``` + +
+👀 Full file preview + +```Python +{!./docs_src/tutorial/indexes/tutorial002.py!} +``` + +
+ +In this case, we want the default value of `age` to continue being `None`, so we set `default=None` when using `Field()`. + +Now when we use **SQLModel** to create the database and tables, it will also create the **indexes** for these two columns in the `hero` table. + +So, when we query the database for the `hero` table and use those **two columns** to define what data we get, the database will be able to **use those indexes** to improve the **reading performance**. 🚀 + +## Primary Key and Indexes + +You probably noticed that we didn't set `index=True` for the `id` field. + +Because the `id` is already the **primary key**, the database will automatically create an internal **index** for it. + +The database always creates an internal index for **primary keys** automatically, as those are the primary way to organize, store, and retrieve data. 🤓 + +But if you want to be **frequently querying** the SQL database for any **other field** (e.g. using any other field in the `WHERE` section), you will probably want to have at least an **index** for that. + +## Recap + +**Indexes** are very important to improve **reading performance** and speed when querying the database. 🏎 + +Creating and using them is very **simple** and easy. The most important part is to understand **how** they work, **when** to create them, and for **which columns**. diff --git a/docs/tutorial/one.md b/docs/tutorial/one.md index 901199bc42..3b60653ed9 100644 --- a/docs/tutorial/one.md +++ b/docs/tutorial/one.md @@ -18,7 +18,7 @@ We'll continue with the same examples we have been using in the previous chapter 👀 Full file preview ```Python -{!./docs_src/tutorial/where/tutorial006.py!} +{!./docs_src/tutorial/indexes/tutorial002.py!} ``` @@ -32,7 +32,7 @@ We have been iterating over the rows in a `result` object like: ```Python hl_lines="7-8" # Code above omitted 👆 -{!./docs_src/tutorial/where/tutorial006.py[ln:44-49]!} +{!./docs_src/tutorial/indexes/tutorial002.py[ln:44-49]!} # Code below omitted 👇 ``` @@ -41,7 +41,7 @@ We have been iterating over the rows in a `result` object like: 👀 Full file preview ```Python -{!./docs_src/tutorial/where/tutorial006.py!} +{!./docs_src/tutorial/indexes/tutorial002.py!} ``` diff --git a/docs/tutorial/update.md b/docs/tutorial/update.md index 420616d78a..b3099f5a16 100644 --- a/docs/tutorial/update.md +++ b/docs/tutorial/update.md @@ -10,7 +10,7 @@ As before, we'll continue from where we left off with the previous code. 👀 Full file preview ```Python -{!./docs_src/tutorial/where/tutorial006.py!} +{!./docs_src/tutorial/indexes/tutorial002.py!} ``` diff --git a/docs/tutorial/where.md b/docs/tutorial/where.md index fd807127cc..45e909cc75 100644 --- a/docs/tutorial/where.md +++ b/docs/tutorial/where.md @@ -233,7 +233,7 @@ The object returned by `select(Hero)` is a special type of object with some meth One of those methods is `.where()` used to (unsurprisingly) add a `WHERE` to the SQL statement in that **select** object. -There are other methods that will will explore later. 💡 +There are other methods that we will explore later. 💡 Most of these methods return the same object again after modifying it. @@ -698,7 +698,7 @@ age=35 id=5 name='Black Lion' secret_name='Trevor Challa' Here's a good moment to see that being able to use these pure Python expressions instead of keyword arguments can help a lot. ✨ -We can use the same standard Python comparison operators like `. +We can use the same standard Python comparison operators like `<`, `<=`, `>`, `>=`, `==`, etc. ## Multiple `.where()` @@ -933,3 +933,7 @@ And with that the editor knows this code is actually fine, because this is a spe ## Recap You can use `.where()` with powerful expressions using **SQLModel** columns (the special class attributes) to filter the rows that you want. 🚀 + +Up to now, the database would have been **looking through each one of the records** (rows) to find the ones that match what you want. If you have thousands or millions of records, this could be very **slow**. 😱 + +In the next section I'll tell you how to add **indexes** to the database, this is what will make the queries **very efficient**. 😎 diff --git a/docs_src/advanced/decimal/tutorial001.py b/docs_src/advanced/decimal/tutorial001.py index 1b16770cc6..b803119d9e 100644 --- a/docs_src/advanced/decimal/tutorial001.py +++ b/docs_src/advanced/decimal/tutorial001.py @@ -6,9 +6,9 @@ class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) money: condecimal(max_digits=5, decimal_places=3) = Field(default=0) diff --git a/docs_src/tutorial/code_structure/tutorial001/models.py b/docs_src/tutorial/code_structure/tutorial001/models.py index 9bd1fa93f2..8e2647b3c4 100644 --- a/docs_src/tutorial/code_structure/tutorial001/models.py +++ b/docs_src/tutorial/code_structure/tutorial001/models.py @@ -5,7 +5,7 @@ class Team(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) headquarters: str heroes: List["Hero"] = Relationship(back_populates="team") @@ -13,9 +13,9 @@ class Team(SQLModel, table=True): class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) team_id: Optional[int] = Field(default=None, foreign_key="team.id") team: Optional[Team] = Relationship(back_populates="heroes") diff --git a/docs_src/tutorial/code_structure/tutorial002/hero_model.py b/docs_src/tutorial/code_structure/tutorial002/hero_model.py index 84fc7f276b..06dd9c6dfd 100644 --- a/docs_src/tutorial/code_structure/tutorial002/hero_model.py +++ b/docs_src/tutorial/code_structure/tutorial002/hero_model.py @@ -8,9 +8,9 @@ class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) team_id: Optional[int] = Field(default=None, foreign_key="team.id") team: Optional["Team"] = Relationship(back_populates="heroes") diff --git a/docs_src/tutorial/code_structure/tutorial002/team_model.py b/docs_src/tutorial/code_structure/tutorial002/team_model.py index 54974a01e5..c8a008bf4c 100644 --- a/docs_src/tutorial/code_structure/tutorial002/team_model.py +++ b/docs_src/tutorial/code_structure/tutorial002/team_model.py @@ -8,7 +8,7 @@ class Team(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) headquarters: str heroes: List["Hero"] = Relationship(back_populates="team") diff --git a/docs_src/tutorial/connect/create_tables/tutorial001.py b/docs_src/tutorial/connect/create_tables/tutorial001.py index 86dcc9adb8..ef24ec77d0 100644 --- a/docs_src/tutorial/connect/create_tables/tutorial001.py +++ b/docs_src/tutorial/connect/create_tables/tutorial001.py @@ -5,15 +5,15 @@ class Team(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) headquarters: str class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) team_id: Optional[int] = Field(default=None, foreign_key="team.id") diff --git a/docs_src/tutorial/connect/delete/tutorial001.py b/docs_src/tutorial/connect/delete/tutorial001.py index 57bbd0ee91..eeb376a0cc 100644 --- a/docs_src/tutorial/connect/delete/tutorial001.py +++ b/docs_src/tutorial/connect/delete/tutorial001.py @@ -5,15 +5,15 @@ class Team(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) headquarters: str class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) team_id: Optional[int] = Field(default=None, foreign_key="team.id") diff --git a/docs_src/tutorial/connect/insert/tutorial001.py b/docs_src/tutorial/connect/insert/tutorial001.py index d64d37f6a7..dc3661d7c7 100644 --- a/docs_src/tutorial/connect/insert/tutorial001.py +++ b/docs_src/tutorial/connect/insert/tutorial001.py @@ -5,15 +5,15 @@ class Team(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) headquarters: str class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) team_id: Optional[int] = Field(default=None, foreign_key="team.id") diff --git a/docs_src/tutorial/connect/select/tutorial001.py b/docs_src/tutorial/connect/select/tutorial001.py index 18c4f402d4..d4cdf413f1 100644 --- a/docs_src/tutorial/connect/select/tutorial001.py +++ b/docs_src/tutorial/connect/select/tutorial001.py @@ -5,15 +5,15 @@ class Team(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) headquarters: str class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) team_id: Optional[int] = Field(default=None, foreign_key="team.id") diff --git a/docs_src/tutorial/connect/select/tutorial002.py b/docs_src/tutorial/connect/select/tutorial002.py index f7df277d65..59edbf7fd9 100644 --- a/docs_src/tutorial/connect/select/tutorial002.py +++ b/docs_src/tutorial/connect/select/tutorial002.py @@ -5,15 +5,15 @@ class Team(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) headquarters: str class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) team_id: Optional[int] = Field(default=None, foreign_key="team.id") diff --git a/docs_src/tutorial/connect/select/tutorial003.py b/docs_src/tutorial/connect/select/tutorial003.py index 110cace997..fb5b8aa0c9 100644 --- a/docs_src/tutorial/connect/select/tutorial003.py +++ b/docs_src/tutorial/connect/select/tutorial003.py @@ -5,15 +5,15 @@ class Team(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) headquarters: str class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) team_id: Optional[int] = Field(default=None, foreign_key="team.id") diff --git a/docs_src/tutorial/connect/select/tutorial004.py b/docs_src/tutorial/connect/select/tutorial004.py index 87e739a91e..d1d260b3f4 100644 --- a/docs_src/tutorial/connect/select/tutorial004.py +++ b/docs_src/tutorial/connect/select/tutorial004.py @@ -5,15 +5,15 @@ class Team(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) headquarters: str class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) team_id: Optional[int] = Field(default=None, foreign_key="team.id") diff --git a/docs_src/tutorial/connect/select/tutorial005.py b/docs_src/tutorial/connect/select/tutorial005.py index 0e696d0382..a61ef8a015 100644 --- a/docs_src/tutorial/connect/select/tutorial005.py +++ b/docs_src/tutorial/connect/select/tutorial005.py @@ -5,15 +5,15 @@ class Team(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) headquarters: str class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) team_id: Optional[int] = Field(default=None, foreign_key="team.id") diff --git a/docs_src/tutorial/connect/update/tutorial001.py b/docs_src/tutorial/connect/update/tutorial001.py index 3c9726f9ef..0080340532 100644 --- a/docs_src/tutorial/connect/update/tutorial001.py +++ b/docs_src/tutorial/connect/update/tutorial001.py @@ -5,15 +5,15 @@ class Team(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) headquarters: str class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) team_id: Optional[int] = Field(default=None, foreign_key="team.id") diff --git a/docs_src/tutorial/delete/tutorial001.py b/docs_src/tutorial/delete/tutorial001.py index 0f7c056174..7c911df50e 100644 --- a/docs_src/tutorial/delete/tutorial001.py +++ b/docs_src/tutorial/delete/tutorial001.py @@ -5,9 +5,9 @@ class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) sqlite_file_name = "database.db" diff --git a/docs_src/tutorial/delete/tutorial002.py b/docs_src/tutorial/delete/tutorial002.py index 1f2671162e..202d63b6d3 100644 --- a/docs_src/tutorial/delete/tutorial002.py +++ b/docs_src/tutorial/delete/tutorial002.py @@ -5,9 +5,9 @@ class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) sqlite_file_name = "database.db" diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py b/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py index 02f5ab8497..88b8fbbcea 100644 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py @@ -5,9 +5,9 @@ class HeroBase(SQLModel): - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) class Hero(HeroBase, table=True): diff --git a/docs_src/tutorial/fastapi/delete/tutorial001.py b/docs_src/tutorial/fastapi/delete/tutorial001.py index 19ab2bb3ca..3c15efbb2d 100644 --- a/docs_src/tutorial/fastapi/delete/tutorial001.py +++ b/docs_src/tutorial/fastapi/delete/tutorial001.py @@ -5,9 +5,9 @@ class HeroBase(SQLModel): - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) class Hero(HeroBase, table=True): diff --git a/docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py b/docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py index 9bdf60446a..aef21332a7 100644 --- a/docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py +++ b/docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py @@ -5,9 +5,9 @@ class HeroBase(SQLModel): - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) class Hero(HeroBase, table=True): diff --git a/docs_src/tutorial/fastapi/multiple_models/tutorial001.py b/docs_src/tutorial/fastapi/multiple_models/tutorial001.py index c46448be80..df20123333 100644 --- a/docs_src/tutorial/fastapi/multiple_models/tutorial001.py +++ b/docs_src/tutorial/fastapi/multiple_models/tutorial001.py @@ -6,9 +6,9 @@ class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) class HeroCreate(SQLModel): diff --git a/docs_src/tutorial/fastapi/multiple_models/tutorial002.py b/docs_src/tutorial/fastapi/multiple_models/tutorial002.py index 537e89637a..392c2c5829 100644 --- a/docs_src/tutorial/fastapi/multiple_models/tutorial002.py +++ b/docs_src/tutorial/fastapi/multiple_models/tutorial002.py @@ -5,9 +5,9 @@ class HeroBase(SQLModel): - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) class Hero(HeroBase, table=True): diff --git a/docs_src/tutorial/fastapi/read_one/tutorial001.py b/docs_src/tutorial/fastapi/read_one/tutorial001.py index bc83db5e04..4d66e471a5 100644 --- a/docs_src/tutorial/fastapi/read_one/tutorial001.py +++ b/docs_src/tutorial/fastapi/read_one/tutorial001.py @@ -5,9 +5,9 @@ class HeroBase(SQLModel): - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) class Hero(HeroBase, table=True): diff --git a/docs_src/tutorial/fastapi/relationships/tutorial001.py b/docs_src/tutorial/fastapi/relationships/tutorial001.py index da21e4686f..97220b95e5 100644 --- a/docs_src/tutorial/fastapi/relationships/tutorial001.py +++ b/docs_src/tutorial/fastapi/relationships/tutorial001.py @@ -5,7 +5,7 @@ class TeamBase(SQLModel): - name: str + name: str = Field(index=True) headquarters: str @@ -30,9 +30,9 @@ class TeamUpdate(SQLModel): class HeroBase(SQLModel): - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) team_id: Optional[int] = Field(default=None, foreign_key="team.id") diff --git a/docs_src/tutorial/fastapi/response_model/tutorial001.py b/docs_src/tutorial/fastapi/response_model/tutorial001.py index 5f7a19064e..57d8738395 100644 --- a/docs_src/tutorial/fastapi/response_model/tutorial001.py +++ b/docs_src/tutorial/fastapi/response_model/tutorial001.py @@ -6,9 +6,9 @@ class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) sqlite_file_name = "database.db" diff --git a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py index 02f5ab8497..88b8fbbcea 100644 --- a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py +++ b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py @@ -5,9 +5,9 @@ class HeroBase(SQLModel): - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) class Hero(HeroBase, table=True): diff --git a/docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py b/docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py index 715836ce57..41eaa621d3 100644 --- a/docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py +++ b/docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py @@ -6,9 +6,9 @@ class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) sqlite_file_name = "database.db" diff --git a/docs_src/tutorial/fastapi/teams/tutorial001.py b/docs_src/tutorial/fastapi/teams/tutorial001.py index dc3e0939e2..e8f88b8e9e 100644 --- a/docs_src/tutorial/fastapi/teams/tutorial001.py +++ b/docs_src/tutorial/fastapi/teams/tutorial001.py @@ -5,7 +5,7 @@ class TeamBase(SQLModel): - name: str + name: str = Field(index=True) headquarters: str @@ -30,9 +30,9 @@ class TeamUpdate(SQLModel): class HeroBase(SQLModel): - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) team_id: Optional[int] = Field(default=None, foreign_key="team.id") diff --git a/docs_src/tutorial/fastapi/update/tutorial001.py b/docs_src/tutorial/fastapi/update/tutorial001.py index 9309d626b7..35554878db 100644 --- a/docs_src/tutorial/fastapi/update/tutorial001.py +++ b/docs_src/tutorial/fastapi/update/tutorial001.py @@ -5,9 +5,9 @@ class HeroBase(SQLModel): - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) class Hero(HeroBase, table=True): diff --git a/docs_src/tutorial/indexes/__init__.py b/docs_src/tutorial/indexes/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/tutorial/indexes/tutorial001.py b/docs_src/tutorial/indexes/tutorial001.py new file mode 100644 index 0000000000..539220c9b7 --- /dev/null +++ b/docs_src/tutorial/indexes/tutorial001.py @@ -0,0 +1,51 @@ +from typing import Optional + +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Deadpond") + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/indexes/tutorial002.py b/docs_src/tutorial/indexes/tutorial002.py new file mode 100644 index 0000000000..ebc8d64f65 --- /dev/null +++ b/docs_src/tutorial/indexes/tutorial002.py @@ -0,0 +1,59 @@ +from typing import Optional + +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.age <= 35) + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/many_to_many/tutorial001.py b/docs_src/tutorial/many_to_many/tutorial001.py index aee77af3b1..bb4e9d0896 100644 --- a/docs_src/tutorial/many_to_many/tutorial001.py +++ b/docs_src/tutorial/many_to_many/tutorial001.py @@ -14,7 +14,7 @@ class HeroTeamLink(SQLModel, table=True): class Team(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) headquarters: str heroes: List["Hero"] = Relationship(back_populates="teams", link_model=HeroTeamLink) @@ -22,9 +22,9 @@ class Team(SQLModel, table=True): class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) teams: List[Team] = Relationship(back_populates="heroes", link_model=HeroTeamLink) diff --git a/docs_src/tutorial/many_to_many/tutorial002.py b/docs_src/tutorial/many_to_many/tutorial002.py index 123fa5a523..dc4aa0b770 100644 --- a/docs_src/tutorial/many_to_many/tutorial002.py +++ b/docs_src/tutorial/many_to_many/tutorial002.py @@ -14,7 +14,7 @@ class HeroTeamLink(SQLModel, table=True): class Team(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) headquarters: str heroes: List["Hero"] = Relationship(back_populates="teams", link_model=HeroTeamLink) @@ -22,9 +22,9 @@ class Team(SQLModel, table=True): class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) teams: List[Team] = Relationship(back_populates="heroes", link_model=HeroTeamLink) diff --git a/docs_src/tutorial/many_to_many/tutorial003.py b/docs_src/tutorial/many_to_many/tutorial003.py index c2f3d56d33..1e03c4af89 100644 --- a/docs_src/tutorial/many_to_many/tutorial003.py +++ b/docs_src/tutorial/many_to_many/tutorial003.py @@ -18,7 +18,7 @@ class HeroTeamLink(SQLModel, table=True): class Team(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) headquarters: str hero_links: List[HeroTeamLink] = Relationship(back_populates="team") @@ -26,9 +26,9 @@ class Team(SQLModel, table=True): class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) team_links: List[HeroTeamLink] = Relationship(back_populates="hero") diff --git a/docs_src/tutorial/offset_and_limit/tutorial001.py b/docs_src/tutorial/offset_and_limit/tutorial001.py index 5413c17ea3..1b033accb9 100644 --- a/docs_src/tutorial/offset_and_limit/tutorial001.py +++ b/docs_src/tutorial/offset_and_limit/tutorial001.py @@ -5,9 +5,9 @@ class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) sqlite_file_name = "database.db" diff --git a/docs_src/tutorial/offset_and_limit/tutorial002.py b/docs_src/tutorial/offset_and_limit/tutorial002.py index 9d85a1342b..65a62369f4 100644 --- a/docs_src/tutorial/offset_and_limit/tutorial002.py +++ b/docs_src/tutorial/offset_and_limit/tutorial002.py @@ -5,9 +5,9 @@ class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) sqlite_file_name = "database.db" diff --git a/docs_src/tutorial/offset_and_limit/tutorial003.py b/docs_src/tutorial/offset_and_limit/tutorial003.py index 5d2c3bffca..36cae9c42c 100644 --- a/docs_src/tutorial/offset_and_limit/tutorial003.py +++ b/docs_src/tutorial/offset_and_limit/tutorial003.py @@ -5,9 +5,9 @@ class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) sqlite_file_name = "database.db" diff --git a/docs_src/tutorial/offset_and_limit/tutorial004.py b/docs_src/tutorial/offset_and_limit/tutorial004.py index bfa882d352..a95715cd98 100644 --- a/docs_src/tutorial/offset_and_limit/tutorial004.py +++ b/docs_src/tutorial/offset_and_limit/tutorial004.py @@ -5,9 +5,9 @@ class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) sqlite_file_name = "database.db" diff --git a/docs_src/tutorial/one/tutorial001.py b/docs_src/tutorial/one/tutorial001.py index 9fa5f0b503..072f4a3bf5 100644 --- a/docs_src/tutorial/one/tutorial001.py +++ b/docs_src/tutorial/one/tutorial001.py @@ -5,9 +5,9 @@ class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) sqlite_file_name = "database.db" diff --git a/docs_src/tutorial/one/tutorial002.py b/docs_src/tutorial/one/tutorial002.py index a1d86e0bbf..c24490659f 100644 --- a/docs_src/tutorial/one/tutorial002.py +++ b/docs_src/tutorial/one/tutorial002.py @@ -5,9 +5,9 @@ class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) sqlite_file_name = "database.db" diff --git a/docs_src/tutorial/one/tutorial003.py b/docs_src/tutorial/one/tutorial003.py index fe8c05cf50..f8fb0bcd7d 100644 --- a/docs_src/tutorial/one/tutorial003.py +++ b/docs_src/tutorial/one/tutorial003.py @@ -5,9 +5,9 @@ class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) sqlite_file_name = "database.db" diff --git a/docs_src/tutorial/one/tutorial004.py b/docs_src/tutorial/one/tutorial004.py index 32bc9b9efc..8688fc4799 100644 --- a/docs_src/tutorial/one/tutorial004.py +++ b/docs_src/tutorial/one/tutorial004.py @@ -5,9 +5,9 @@ class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) sqlite_file_name = "database.db" diff --git a/docs_src/tutorial/one/tutorial005.py b/docs_src/tutorial/one/tutorial005.py index 238213670e..f624d4cb68 100644 --- a/docs_src/tutorial/one/tutorial005.py +++ b/docs_src/tutorial/one/tutorial005.py @@ -5,9 +5,9 @@ class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) sqlite_file_name = "database.db" diff --git a/docs_src/tutorial/one/tutorial006.py b/docs_src/tutorial/one/tutorial006.py index cf408c494a..afc48547d9 100644 --- a/docs_src/tutorial/one/tutorial006.py +++ b/docs_src/tutorial/one/tutorial006.py @@ -5,9 +5,9 @@ class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) sqlite_file_name = "database.db" diff --git a/docs_src/tutorial/one/tutorial007.py b/docs_src/tutorial/one/tutorial007.py index 8a36d978a4..15df5baa94 100644 --- a/docs_src/tutorial/one/tutorial007.py +++ b/docs_src/tutorial/one/tutorial007.py @@ -5,9 +5,9 @@ class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) sqlite_file_name = "database.db" diff --git a/docs_src/tutorial/one/tutorial008.py b/docs_src/tutorial/one/tutorial008.py index 1b0d6ef501..9aa077ca38 100644 --- a/docs_src/tutorial/one/tutorial008.py +++ b/docs_src/tutorial/one/tutorial008.py @@ -5,9 +5,9 @@ class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) sqlite_file_name = "database.db" diff --git a/docs_src/tutorial/one/tutorial009.py b/docs_src/tutorial/one/tutorial009.py index 70deed28a8..f4ee23b355 100644 --- a/docs_src/tutorial/one/tutorial009.py +++ b/docs_src/tutorial/one/tutorial009.py @@ -5,9 +5,9 @@ class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) sqlite_file_name = "database.db" diff --git a/docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py b/docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py index d9851b4e43..fc4eb97934 100644 --- a/docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py +++ b/docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py @@ -5,7 +5,7 @@ class Team(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) headquarters: str heroes: List["Hero"] = Relationship() @@ -13,9 +13,9 @@ class Team(SQLModel, table=True): class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) team_id: Optional[int] = Field(default=None, foreign_key="team.id") team: Optional[Team] = Relationship() diff --git a/docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py b/docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py index b33fabe716..a25df4e75d 100644 --- a/docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py +++ b/docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py @@ -5,7 +5,7 @@ class Team(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) headquarters: str heroes: List["Hero"] = Relationship(back_populates="team") @@ -13,9 +13,9 @@ class Team(SQLModel, table=True): class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) team_id: Optional[int] = Field(default=None, foreign_key="team.id") team: Optional[Team] = Relationship(back_populates="heroes") diff --git a/docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py b/docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py index cbd1581b10..c137f58f6a 100644 --- a/docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py +++ b/docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py @@ -5,14 +5,14 @@ class Weapon(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) hero: "Hero" = Relationship(back_populates="weapon") class Power(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) hero_id: int = Field(foreign_key="hero.id") hero: "Hero" = Relationship(back_populates="powers") @@ -20,7 +20,7 @@ class Power(SQLModel, table=True): class Team(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) headquarters: str heroes: List["Hero"] = Relationship(back_populates="team") @@ -28,9 +28,9 @@ class Team(SQLModel, table=True): class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) team_id: Optional[int] = Field(default=None, foreign_key="team.id") team: Optional[Team] = Relationship(back_populates="heroes") diff --git a/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py b/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py index 2bf2041ff9..ec9c909d73 100644 --- a/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py +++ b/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py @@ -5,7 +5,7 @@ class Team(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) headquarters: str heroes: List["Hero"] = Relationship(back_populates="team") @@ -13,9 +13,9 @@ class Team(SQLModel, table=True): class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) team_id: Optional[int] = Field(default=None, foreign_key="team.id") team: Optional[Team] = Relationship(back_populates="heroes") diff --git a/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py b/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py index 98c1919209..71cb3f6136 100644 --- a/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py +++ b/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py @@ -5,7 +5,7 @@ class Team(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) headquarters: str heroes: List["Hero"] = Relationship(back_populates="team") @@ -13,9 +13,9 @@ class Team(SQLModel, table=True): class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) team_id: Optional[int] = Field(default=None, foreign_key="team.id") team: Optional[Team] = Relationship(back_populates="heroes") diff --git a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py index e5c23a7264..5f718cab45 100644 --- a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py +++ b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py @@ -5,7 +5,7 @@ class Team(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) headquarters: str heroes: List["Hero"] = Relationship(back_populates="team") @@ -13,9 +13,9 @@ class Team(SQLModel, table=True): class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) team_id: Optional[int] = Field(default=None, foreign_key="team.id") team: Optional[Team] = Relationship(back_populates="heroes") diff --git a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py index efae8e556c..fdb436eb5f 100644 --- a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py +++ b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py @@ -5,7 +5,7 @@ class Team(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) headquarters: str heroes: List["Hero"] = Relationship(back_populates="team") @@ -13,9 +13,9 @@ class Team(SQLModel, table=True): class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) team_id: Optional[int] = Field(default=None, foreign_key="team.id") team: Optional[Team] = Relationship(back_populates="heroes") diff --git a/docs_src/tutorial/update/tutorial001.py b/docs_src/tutorial/update/tutorial001.py index 96c72088fa..37868acc96 100644 --- a/docs_src/tutorial/update/tutorial001.py +++ b/docs_src/tutorial/update/tutorial001.py @@ -5,9 +5,9 @@ class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) sqlite_file_name = "database.db" diff --git a/docs_src/tutorial/update/tutorial002.py b/docs_src/tutorial/update/tutorial002.py index 04185f8e76..4880f73f90 100644 --- a/docs_src/tutorial/update/tutorial002.py +++ b/docs_src/tutorial/update/tutorial002.py @@ -5,9 +5,9 @@ class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) sqlite_file_name = "database.db" diff --git a/docs_src/tutorial/update/tutorial003.py b/docs_src/tutorial/update/tutorial003.py index c3199152bd..fd2ed75f0b 100644 --- a/docs_src/tutorial/update/tutorial003.py +++ b/docs_src/tutorial/update/tutorial003.py @@ -5,9 +5,9 @@ class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) sqlite_file_name = "database.db" diff --git a/docs_src/tutorial/update/tutorial004.py b/docs_src/tutorial/update/tutorial004.py index e61a04fbec..868c66c7d4 100644 --- a/docs_src/tutorial/update/tutorial004.py +++ b/docs_src/tutorial/update/tutorial004.py @@ -5,9 +5,9 @@ class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - name: str + name: str = Field(index=True) secret_name: str - age: Optional[int] = None + age: Optional[int] = Field(default=None, index=True) sqlite_file_name = "database.db" diff --git a/mkdocs.yml b/mkdocs.yml index 41b44b6931..13744db8fd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -19,6 +19,7 @@ theme: features: - search.suggest - search.highlight + - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: img/icon-white.svg @@ -43,6 +44,7 @@ nav: - tutorial/automatic-id-none-refresh.md - tutorial/select.md - tutorial/where.md + - tutorial/indexes.md - tutorial/one.md - tutorial/limit-and-offset.md - tutorial/update.md @@ -103,7 +105,8 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_div_format '' -- pymdownx.tabbed +- pymdownx.tabbed: + alternate_style: true - mdx_include extra: diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 08eaf5956f..12f30ba129 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -426,7 +426,7 @@ def get_column_from_field(field: ModelField) -> Column: # type: ignore nullable = not field.required index = getattr(field.field_info, "index", Undefined) if index is Undefined: - index = True + index = False if hasattr(field.field_info, "nullable"): field_nullable = getattr(field.field_info, "nullable") if field_nullable != Undefined: diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py index ac85eca2d5..cf008563f4 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py @@ -1,4 +1,6 @@ from fastapi.testclient import TestClient +from sqlalchemy import inspect +from sqlalchemy.engine.reflection import Inspector from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -166,3 +168,16 @@ def test_tutorial(clear_sqlmodel): assert response.status_code == 200, response.text assert data == openapi_schema + + # Test inherited indexes + insp: Inspector = inspect(mod.engine) + indexes = insp.get_indexes(str(mod.Hero.__tablename__)) + expected_indexes = [ + {"name": "ix_hero_name", "column_names": ["name"], "unique": 0}, + {"name": "ix_hero_age", "column_names": ["age"], "unique": 0}, + ] + for index in expected_indexes: + assert index in indexes, "This expected index should be in the indexes in DB" + # Now that this index was checked, remove it from the list of indexes + indexes.pop(indexes.index(index)) + assert len(indexes) == 0, "The database should only have the expected indexes" diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py index 421a1cad53..57393a7ddc 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py @@ -1,4 +1,6 @@ from fastapi.testclient import TestClient +from sqlalchemy import inspect +from sqlalchemy.engine.reflection import Inspector from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -166,3 +168,16 @@ def test_tutorial(clear_sqlmodel): assert response.status_code == 200, response.text assert data == openapi_schema + + # Test inherited indexes + insp: Inspector = inspect(mod.engine) + indexes = insp.get_indexes(str(mod.Hero.__tablename__)) + expected_indexes = [ + {"name": "ix_hero_age", "column_names": ["age"], "unique": 0}, + {"name": "ix_hero_name", "column_names": ["name"], "unique": 0}, + ] + for index in expected_indexes: + assert index in indexes, "This expected index should be in the indexes in DB" + # Now that this index was checked, remove it from the list of indexes + indexes.pop(indexes.index(index)) + assert len(indexes) == 0, "The database should only have the expected indexes" diff --git a/tests/test_tutorial/test_indexes/__init__.py b/tests/test_tutorial/test_indexes/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_tutorial/test_indexes/test_tutorial001.py b/tests/test_tutorial/test_indexes/test_tutorial001.py new file mode 100644 index 0000000000..596207737d --- /dev/null +++ b/tests/test_tutorial/test_indexes/test_tutorial001.py @@ -0,0 +1,35 @@ +from unittest.mock import patch + +from sqlalchemy import inspect +from sqlalchemy.engine.reflection import Inspector +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function + + +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.indexes import tutorial001 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == [ + [{"secret_name": "Dive Wilson", "age": None, "id": 1, "name": "Deadpond"}] + ] + + insp: Inspector = inspect(mod.engine) + indexes = insp.get_indexes(str(mod.Hero.__tablename__)) + expected_indexes = [ + {"name": "ix_hero_name", "column_names": ["name"], "unique": 0}, + {"name": "ix_hero_age", "column_names": ["age"], "unique": 0}, + ] + for index in expected_indexes: + assert index in indexes, "This expected index should be in the indexes in DB" + # Now that this index was checked, remove it from the list of indexes + indexes.pop(indexes.index(index)) + assert len(indexes) == 0, "The database should only have the expected indexes" diff --git a/tests/test_tutorial/test_indexes/test_tutorial006.py b/tests/test_tutorial/test_indexes/test_tutorial006.py new file mode 100644 index 0000000000..e26f8b2ed8 --- /dev/null +++ b/tests/test_tutorial/test_indexes/test_tutorial006.py @@ -0,0 +1,36 @@ +from unittest.mock import patch + +from sqlalchemy import inspect +from sqlalchemy.engine.reflection import Inspector +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function + + +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.indexes import tutorial002 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == [ + [{"name": "Tarantula", "secret_name": "Natalia Roman-on", "age": 32, "id": 4}], + [{"name": "Black Lion", "secret_name": "Trevor Challa", "age": 35, "id": 5}], + ] + + insp: Inspector = inspect(mod.engine) + indexes = insp.get_indexes(str(mod.Hero.__tablename__)) + expected_indexes = [ + {"name": "ix_hero_name", "column_names": ["name"], "unique": 0}, + {"name": "ix_hero_age", "column_names": ["age"], "unique": 0}, + ] + for index in expected_indexes: + assert index in indexes, "This expected index should be in the indexes in DB" + # Now that this index was checked, remove it from the list of indexes + indexes.pop(indexes.index(index)) + assert len(indexes) == 0, "The database should only have the expected indexes" diff --git a/tests/test_tutorial/test_where/test_tutorial003.py b/tests/test_tutorial/test_where/test_tutorial003.py index a01955e6b7..4794d846ff 100644 --- a/tests/test_tutorial/test_where/test_tutorial003.py +++ b/tests/test_tutorial/test_where/test_tutorial003.py @@ -17,7 +17,7 @@ def test_tutorial(clear_sqlmodel): with patch("builtins.print", new=new_print): mod.main() - assert calls == [ + expected_calls = [ [{"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}], [{"id": 3, "name": "Rusty-Man", "secret_name": "Tommy Sharp", "age": 48}], [ @@ -29,3 +29,8 @@ def test_tutorial(clear_sqlmodel): } ], ] + for call in expected_calls: + assert call in calls, "This expected item should be in the list" + # Now that this item was checked, remove it from the list + calls.pop(calls.index(call)) + assert len(calls) == 0, "The list should only have the expected items" diff --git a/tests/test_tutorial/test_where/test_tutorial004.py b/tests/test_tutorial/test_where/test_tutorial004.py index 9f4f80c201..682babd43a 100644 --- a/tests/test_tutorial/test_where/test_tutorial004.py +++ b/tests/test_tutorial/test_where/test_tutorial004.py @@ -16,7 +16,7 @@ def test_tutorial(clear_sqlmodel): with patch("builtins.print", new=new_print): mod.main() - assert calls == [ + expected_calls = [ [{"id": 5, "name": "Black Lion", "secret_name": "Trevor Challa", "age": 35}], [{"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}], [{"id": 3, "name": "Rusty-Man", "secret_name": "Tommy Sharp", "age": 48}], @@ -29,3 +29,8 @@ def test_tutorial(clear_sqlmodel): } ], ] + for call in expected_calls: + assert call in calls, "This expected item should be in the list" + # Now that this item was checked, remove it from the list + calls.pop(calls.index(call)) + assert len(calls) == 0, "The list should only have the expected items" diff --git a/tests/test_tutorial/test_where/test_tutorial011.py b/tests/test_tutorial/test_where/test_tutorial011.py index 743ecd5402..8006cd0708 100644 --- a/tests/test_tutorial/test_where/test_tutorial011.py +++ b/tests/test_tutorial/test_where/test_tutorial011.py @@ -16,7 +16,7 @@ def test_tutorial(clear_sqlmodel): with patch("builtins.print", new=new_print): mod.main() - assert calls == [ + expected_calls = [ [{"id": 5, "name": "Black Lion", "secret_name": "Trevor Challa", "age": 35}], [{"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}], [{"id": 3, "name": "Rusty-Man", "secret_name": "Tommy Sharp", "age": 48}], @@ -29,3 +29,8 @@ def test_tutorial(clear_sqlmodel): } ], ] + for call in expected_calls: + assert call in calls, "This expected item should be in the list" + # Now that this item was checked, remove it from the list + calls.pop(calls.index(call)) + assert len(calls) == 0, "The list should only have the expected items" From d6d77a9ee4aee5bd4e8e38974a081963d6c7394b Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 28 Dec 2021 10:48:37 +0000 Subject: [PATCH 032/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index f26c10c169..f2a66c242f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Document indexes and make them opt-in. PR [#205](https://github.com/tiangolo/sqlmodel/pull/205) by [@tiangolo](https://github.com/tiangolo). * ✏ Fix typo in FastAPI tutorial. PR [#192](https://github.com/tiangolo/sqlmodel/pull/192) by [@yaquelinehoyos](https://github.com/yaquelinehoyos). * 📝 Add links to the license file. PR [#29](https://github.com/tiangolo/sqlmodel/pull/29) by [@sobolevn](https://github.com/sobolevn). * ✏ Fix typos in docs titles. PR [#28](https://github.com/tiangolo/sqlmodel/pull/28) by [@Batalex](https://github.com/Batalex). From 9203df6af10b670b0964deb82665f5b976cebb3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 28 Dec 2021 12:26:52 +0100 Subject: [PATCH 033/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 63 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index f2a66c242f..4008a218fe 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,7 +2,68 @@ ## Latest Changes -* ✨ Document indexes and make them opt-in. PR [#205](https://github.com/tiangolo/sqlmodel/pull/205) by [@tiangolo](https://github.com/tiangolo). +### Breaking Changes + +**SQLModel** no longer creates indexes by default for every column, indexes are now opt-in. You can read more about it in PR [#205](https://github.com/tiangolo/sqlmodel/pull/205). + +Before this change, if you had a model like this: + +```Python +from typing import Optional + +from sqlmodel import Field, SQLModel + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str + secret_name: str + age: Optional[int] = None +``` + +...when creating the tables, SQLModel version `0.0.5` and below, would also create an index for `name`, one for `secret_name`, and one for `age` (`id` is the primary key, so it doesn't need an additional index). + +If you depended on having an index for each one of those columns, now you can (and would have to) define them explicitly: + +```Python +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str = Field(index=True) + age: Optional[int] = Field(default=None, index=True) +``` + +There's a high chance you don't need indexes for all the columns. For example, you might only need indexes for `name` and `age`, but not for `secret_name`. In that case, you could define the model as: + +```Python +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) +``` + +If you already created your database tables with SQLModel using versions `0.0.5` or below, it would have also created those indexes in the database. In that case, you might want to manually drop (remove) some of those indexes, if they are unnecessary, to avoid the extra cost in performance and space. + +Depending on the database you are using, there will be a different way to find the available indexes. + +For example, let's say you no longer need the index for `secret_name`. You could check the current indexes in the database and find the one for `secret_name`, it could be named `ix_hero_secret_name`. Then you can remove it with SQL: + +```SQL +DROP INDEX ix_hero_secret_name +``` + +or + +```SQL +DROP INDEX ix_hero_secret_name ON hero; +``` + +Here's the new, extensive documentation explaining indexes and how to use them: [Indexes - Optimize Queries](https://sqlmodel.tiangolo.com/tutorial/indexes/). + +### Docs + +* ✨ Document indexes and make them opt-in. Here's the new documentation: [Indexes - Optimize Queries](https://sqlmodel.tiangolo.com/tutorial/indexes/). This is the same change described above in **Breaking Changes**. PR [#205](https://github.com/tiangolo/sqlmodel/pull/205) by [@tiangolo](https://github.com/tiangolo). * ✏ Fix typo in FastAPI tutorial. PR [#192](https://github.com/tiangolo/sqlmodel/pull/192) by [@yaquelinehoyos](https://github.com/yaquelinehoyos). * 📝 Add links to the license file. PR [#29](https://github.com/tiangolo/sqlmodel/pull/29) by [@sobolevn](https://github.com/sobolevn). * ✏ Fix typos in docs titles. PR [#28](https://github.com/tiangolo/sqlmodel/pull/28) by [@Batalex](https://github.com/Batalex). From 7fcd4fd7c5661d2621e89ebf3a87059ac91ea6d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 28 Dec 2021 12:27:33 +0100 Subject: [PATCH 034/906] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.0.?= =?UTF-8?q?6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 3 +++ sqlmodel/__init__.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 4008a218fe..e2bef52123 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.0.6 + ### Breaking Changes **SQLModel** no longer creates indexes by default for every column, indexes are now opt-in. You can read more about it in PR [#205](https://github.com/tiangolo/sqlmodel/pull/205). diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index 78f47e21e7..12eb5d569b 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.0.5" +__version__ = "0.0.6" # Re-export from SQLAlchemy from sqlalchemy.engine import create_mock_engine as create_mock_engine From 8d1b6f079adad47cc242710f6cb1790a8ad8fbd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 8 Jan 2022 17:36:19 +0100 Subject: [PATCH 035/906] =?UTF-8?q?=E2=AC=86=20Upgrade=20mypy,=20fix=20typ?= =?UTF-8?q?e=20annotations=20(#218)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- sqlmodel/main.py | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a8355cf1ad..5f22ceaeb9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ sqlalchemy2-stubs = {version = "*", allow-prereleases = true} [tool.poetry.dev-dependencies] pytest = "^6.2.4" -mypy = "^0.910" +mypy = "0.930" flake8 = "^3.9.2" black = {version = "^21.5-beta.1", python = "^3.7"} mkdocs = "^1.2.1" diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 12f30ba129..4d6d2f2712 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -6,7 +6,6 @@ from enum import Enum from pathlib import Path from typing import ( - TYPE_CHECKING, AbstractSet, Any, Callable, @@ -24,11 +23,11 @@ cast, ) -from pydantic import BaseModel +from pydantic import BaseConfig, BaseModel from pydantic.errors import ConfigError, DictError from pydantic.fields import FieldInfo as PydanticFieldInfo from pydantic.fields import ModelField, Undefined, UndefinedType -from pydantic.main import BaseConfig, ModelMetaclass, validate_model +from pydantic.main import ModelMetaclass, validate_model from pydantic.typing import ForwardRef, NoArgAnyCallable, resolve_annotations from pydantic.utils import ROOT_KEY, Representation from sqlalchemy import ( @@ -453,7 +452,7 @@ def get_column_from_field(field: ModelField) -> Column: # type: ignore sa_column_kwargs = getattr(field.field_info, "sa_column_kwargs", Undefined) if sa_column_kwargs is not Undefined: kwargs.update(cast(Dict[Any, Any], sa_column_kwargs)) - return Column(sa_type, *args, **kwargs) + return Column(sa_type, *args, **kwargs) # type: ignore class_registry = weakref.WeakValueDictionary() # type: ignore @@ -494,9 +493,6 @@ def __new__(cls, *args: Any, **kwargs: Any) -> Any: def __init__(__pydantic_self__, **data: Any) -> None: # Uses something other than `self` the first arg to allow "self" as a # settable attribute - if TYPE_CHECKING: - __pydantic_self__.__dict__: Dict[str, Any] = {} - __pydantic_self__.__fields_set__: Set[str] = set() values, fields_set, validation_error = validate_model( __pydantic_self__.__class__, data ) @@ -608,7 +604,7 @@ def validate(cls: Type["SQLModel"], value: Any) -> "SQLModel": return cls(**value_as_dict) # From Pydantic, override to only show keys from fields, omit SQLAlchemy attributes - def _calculate_keys( # type: ignore + def _calculate_keys( self, include: Optional[Mapping[Union[int, str], Any]], exclude: Optional[Mapping[Union[int, str], Any]], From 800a5f232ffbfb1ce266c4ce9359af92bc8d75b0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 8 Jan 2022 16:37:03 +0000 Subject: [PATCH 036/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index e2bef52123..0f646a0746 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Upgrade mypy, fix type annotations. PR [#218](https://github.com/tiangolo/sqlmodel/pull/218) by [@tiangolo](https://github.com/tiangolo). ## 0.0.6 From c873aa3930521765835fc9b88f082aa1759dbadb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 8 Jan 2022 17:49:07 +0100 Subject: [PATCH 037/906] =?UTF-8?q?=F0=9F=94=A7=20Upgrade=20MkDocs=20Mater?= =?UTF-8?q?ial=20and=20update=20configs=20(#217)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mkdocs.yml | 9 ++++----- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 13744db8fd..41a7258a75 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -28,9 +28,6 @@ theme: repo_name: tiangolo/sqlmodel repo_url: https://github.com/tiangolo/sqlmodel edit_uri: '' -google_analytics: - - UA-205713594-2 - - auto nav: - SQLModel: index.md - features.md @@ -104,12 +101,15 @@ markdown_extensions: custom_fences: - name: mermaid class: mermaid - format: !!python/name:pymdownx.superfences.fence_div_format '' + format: !!python/name:pymdownx.superfences.fence_code_format '' - pymdownx.tabbed: alternate_style: true - mdx_include extra: + analytics: + provider: google + property: UA-205713594-2 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/sqlmodel @@ -129,6 +129,5 @@ extra_css: - css/custom.css extra_javascript: - - https://unpkg.com/mermaid@8.4.6/dist/mermaid.min.js - js/termynal.js - js/custom.js diff --git a/pyproject.toml b/pyproject.toml index 5f22ceaeb9..814cba6f7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ mypy = "0.930" flake8 = "^3.9.2" black = {version = "^21.5-beta.1", python = "^3.7"} mkdocs = "^1.2.1" -mkdocs-material = "^7.1.9" +mkdocs-material = "^8.1.4" mdx-include = "^1.4.1" coverage = {extras = ["toml"], version = "^5.5"} fastapi = "^0.68.0" From e6f8c00bbef19787a939e4aee659687b1672f47a Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 8 Jan 2022 16:54:48 +0000 Subject: [PATCH 038/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 0f646a0746..dcd9d75e96 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Upgrade MkDocs Material and update configs. PR [#217](https://github.com/tiangolo/sqlmodel/pull/217) by [@tiangolo](https://github.com/tiangolo). * ⬆ Upgrade mypy, fix type annotations. PR [#218](https://github.com/tiangolo/sqlmodel/pull/218) by [@tiangolo](https://github.com/tiangolo). ## 0.0.6 From 7176d89e487f6268e273b38136820af6e656b07a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 13 Feb 2022 18:14:02 +0100 Subject: [PATCH 039/906] =?UTF-8?q?=F0=9F=92=9A=20Only=20run=20CI=20on=20p?= =?UTF-8?q?ush=20when=20on=20master,=20to=20avoid=20duplicate=20runs=20on?= =?UTF-8?q?=20PRs=20(#244)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 2 ++ .github/workflows/test.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 72a79d19f7..18e35b308e 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -1,6 +1,8 @@ name: Build Docs on: push: + branches: + - main pull_request: types: [opened, synchronize] workflow_dispatch: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6e15a7d6f7..744a0fa250 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,6 +2,8 @@ name: Test on: push: + branches: + - main pull_request: types: [opened, synchronize] workflow_dispatch: From 8e97c93de0c185780c20d752ac947854e652c754 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 13 Feb 2022 17:14:34 +0000 Subject: [PATCH 040/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index dcd9d75e96..df4b54c546 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 💚 Only run CI on push when on master, to avoid duplicate runs on PRs. PR [#244](https://github.com/tiangolo/sqlmodel/pull/244) by [@tiangolo](https://github.com/tiangolo). * 🔧 Upgrade MkDocs Material and update configs. PR [#217](https://github.com/tiangolo/sqlmodel/pull/217) by [@tiangolo](https://github.com/tiangolo). * ⬆ Upgrade mypy, fix type annotations. PR [#218](https://github.com/tiangolo/sqlmodel/pull/218) by [@tiangolo](https://github.com/tiangolo). From 03e861d048f04e91c5526547ced7e6de5f8ccf64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 16 Apr 2022 11:13:19 +0200 Subject: [PATCH 041/906] =?UTF-8?q?=E2=9C=A8=20Add=20new=20Session.get()?= =?UTF-8?q?=20parameter=20execution=5Foptions=20(#302)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/orm/session.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sqlmodel/orm/session.py b/sqlmodel/orm/session.py index 453e0eefaf..1692fdcbcb 100644 --- a/sqlmodel/orm/session.py +++ b/sqlmodel/orm/session.py @@ -128,6 +128,7 @@ def get( populate_existing: bool = False, with_for_update: Optional[Union[Literal[True], Mapping[str, Any]]] = None, identity_token: Optional[Any] = None, + execution_options: Optional[Mapping[Any, Any]] = util.EMPTY_DICT, ) -> Optional[_TSelectParam]: return super().get( entity, @@ -136,4 +137,5 @@ def get( populate_existing=populate_existing, with_for_update=with_for_update, identity_token=identity_token, + execution_options=execution_options, ) From e009ecb704c68e6b2c37bf1bd63f1d526e080607 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 16 Apr 2022 09:13:48 +0000 Subject: [PATCH 042/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index df4b54c546..2117851537 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Add new `Session.get()` parameter `execution_options`. PR [#302](https://github.com/tiangolo/sqlmodel/pull/302) by [@tiangolo](https://github.com/tiangolo). * 💚 Only run CI on push when on master, to avoid duplicate runs on PRs. PR [#244](https://github.com/tiangolo/sqlmodel/pull/244) by [@tiangolo](https://github.com/tiangolo). * 🔧 Upgrade MkDocs Material and update configs. PR [#217](https://github.com/tiangolo/sqlmodel/pull/217) by [@tiangolo](https://github.com/tiangolo). * ⬆ Upgrade mypy, fix type annotations. PR [#218](https://github.com/tiangolo/sqlmodel/pull/218) by [@tiangolo](https://github.com/tiangolo). From b94d393924d997f8913b2c11f18b4c729224ae91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 16 Apr 2022 11:19:30 +0200 Subject: [PATCH 043/906] =?UTF-8?q?=F0=9F=91=B7=20Upgrade=20Codecov=20GitH?= =?UTF-8?q?ub=20Action=20(#304)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 744a0fa250..5b772ed3e3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,4 +59,4 @@ jobs: - name: Test run: python -m poetry run bash scripts/test.sh - name: Upload coverage - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v2 From 6d969c58459846b9bc0111ad482a4aa030440c0d Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 16 Apr 2022 09:20:01 +0000 Subject: [PATCH 044/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 2117851537..0231504fd0 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Upgrade Codecov GitHub Action. PR [#304](https://github.com/tiangolo/sqlmodel/pull/304) by [@tiangolo](https://github.com/tiangolo). * ✨ Add new `Session.get()` parameter `execution_options`. PR [#302](https://github.com/tiangolo/sqlmodel/pull/302) by [@tiangolo](https://github.com/tiangolo). * 💚 Only run CI on push when on master, to avoid duplicate runs on PRs. PR [#244](https://github.com/tiangolo/sqlmodel/pull/244) by [@tiangolo](https://github.com/tiangolo). * 🔧 Upgrade MkDocs Material and update configs. PR [#217](https://github.com/tiangolo/sqlmodel/pull/217) by [@tiangolo](https://github.com/tiangolo). From e523e1e4c3edae28b87046c1e0d59f19e94b3c3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 16 Apr 2022 11:24:53 +0200 Subject: [PATCH 045/906] =?UTF-8?q?=F0=9F=93=9D=20Add=20Jina's=20QA=20Bot?= =?UTF-8?q?=20to=20the=20docs=20to=20help=20people=20that=20want=20to=20as?= =?UTF-8?q?k=20quick=20questions=20(#263)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: yanlong.wang Co-authored-by: Han Xiao --- docs/overrides/main.html | 31 +++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 32 insertions(+) create mode 100644 docs/overrides/main.html diff --git a/docs/overrides/main.html b/docs/overrides/main.html new file mode 100644 index 0000000000..fc5bce873f --- /dev/null +++ b/docs/overrides/main.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} +{%- block scripts %} +{{ super() }} + + + + + +{%- endblock %} \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 41a7258a75..a27bbde8a1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,6 +3,7 @@ site_description: SQLModel, SQL databases in Python, designed for simplicity, co site_url: https://sqlmodel.tiangolo.com/ theme: name: material + custom_dir: docs/overrides palette: - scheme: default primary: deep purple From d6229b39371734a9decf1e49ea33d81414eb80db Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 16 Apr 2022 09:25:22 +0000 Subject: [PATCH 046/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 0231504fd0..9f934b441b 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Add Jina's QA Bot to the docs to help people that want to ask quick questions. PR [#263](https://github.com/tiangolo/sqlmodel/pull/263) by [@tiangolo](https://github.com/tiangolo). * 👷 Upgrade Codecov GitHub Action. PR [#304](https://github.com/tiangolo/sqlmodel/pull/304) by [@tiangolo](https://github.com/tiangolo). * ✨ Add new `Session.get()` parameter `execution_options`. PR [#302](https://github.com/tiangolo/sqlmodel/pull/302) by [@tiangolo](https://github.com/tiangolo). * 💚 Only run CI on push when on master, to avoid duplicate runs on PRs. PR [#244](https://github.com/tiangolo/sqlmodel/pull/244) by [@tiangolo](https://github.com/tiangolo). From 88683f6e2c01467eb55c27005a8a55d43612d9c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 16 Apr 2022 11:30:19 +0200 Subject: [PATCH 047/906] =?UTF-8?q?=F0=9F=91=B7=20Add=20CI=20for=20Python?= =?UTF-8?q?=203.10=20(#305)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5b772ed3e3..c135ff3915 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] fail-fast: false steps: From 4d200517934c38b44c8ce372425a1db67917a545 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 16 Apr 2022 09:30:54 +0000 Subject: [PATCH 048/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 9f934b441b..1fc20bc13f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Add CI for Python 3.10. PR [#305](https://github.com/tiangolo/sqlmodel/pull/305) by [@tiangolo](https://github.com/tiangolo). * 📝 Add Jina's QA Bot to the docs to help people that want to ask quick questions. PR [#263](https://github.com/tiangolo/sqlmodel/pull/263) by [@tiangolo](https://github.com/tiangolo). * 👷 Upgrade Codecov GitHub Action. PR [#304](https://github.com/tiangolo/sqlmodel/pull/304) by [@tiangolo](https://github.com/tiangolo). * ✨ Add new `Session.get()` parameter `execution_options`. PR [#302](https://github.com/tiangolo/sqlmodel/pull/302) by [@tiangolo](https://github.com/tiangolo). From 4dd7b890d41fe926530188030a98046b5b8b31b8 Mon Sep 17 00:00:00 2001 From: byrman Date: Sat, 27 Aug 2022 20:10:38 +0200 Subject: [PATCH 049/906] =?UTF-8?q?=F0=9F=90=9B=20Fix=20SQLAlchemy=20versi?= =?UTF-8?q?on=201.4.36=20breaks=20SQLModel=20relationships=20(#315)=20(#32?= =?UTF-8?q?2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- sqlmodel/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 4d6d2f2712..63c6dcbe5f 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -369,6 +369,7 @@ def __init__( relationship_to, *rel_args, **rel_kwargs ) dict_used[rel_name] = rel_value + setattr(cls, rel_name, rel_value) # Fix #315 DeclarativeMeta.__init__(cls, classname, bases, dict_used, **kw) else: ModelMetaclass.__init__(cls, classname, bases, dict_, **kw) From ea18162391897e41da2704244b186d53d43d83f3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 18:11:12 +0000 Subject: [PATCH 050/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 1fc20bc13f..bbbf83b39f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix SQLAlchemy version 1.4.36 breaks SQLModel relationships (#315). PR [#322](https://github.com/tiangolo/sqlmodel/pull/322) by [@byrman](https://github.com/byrman). * 👷 Add CI for Python 3.10. PR [#305](https://github.com/tiangolo/sqlmodel/pull/305) by [@tiangolo](https://github.com/tiangolo). * 📝 Add Jina's QA Bot to the docs to help people that want to ask quick questions. PR [#263](https://github.com/tiangolo/sqlmodel/pull/263) by [@tiangolo](https://github.com/tiangolo). * 👷 Upgrade Codecov GitHub Action. PR [#304](https://github.com/tiangolo/sqlmodel/pull/304) by [@tiangolo](https://github.com/tiangolo). From c830c71e2850e2c01290b55e90c989c416d18ebe Mon Sep 17 00:00:00 2001 From: Robert Rosca <32569096+RobertRosca@users.noreply.github.com> Date: Sat, 27 Aug 2022 20:21:38 +0200 Subject: [PATCH 051/906] =?UTF-8?q?=E2=AC=86=20Upgrade=20constrain=20for?= =?UTF-8?q?=20SQLAlchemy=20=3D=20">=3D1.4.17,<=3D1.4.41"=20(#371)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 814cba6f7b..d2ecfb785d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ classifiers = [ [tool.poetry.dependencies] python = "^3.6.1" -SQLAlchemy = ">=1.4.17,<1.5.0" +SQLAlchemy = ">=1.4.17,<=1.4.41" pydantic = "^1.8.2" sqlalchemy2-stubs = {version = "*", allow-prereleases = true} From f4500c6ba4dd4bbaba1fe071abca7cb0c76e18e5 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 18:22:18 +0000 Subject: [PATCH 052/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index bbbf83b39f..a189538e29 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Upgrade constrain for SQLAlchemy = ">=1.4.17,<=1.4.41". PR [#371](https://github.com/tiangolo/sqlmodel/pull/371) by [@RobertRosca](https://github.com/RobertRosca). * 🐛 Fix SQLAlchemy version 1.4.36 breaks SQLModel relationships (#315). PR [#322](https://github.com/tiangolo/sqlmodel/pull/322) by [@byrman](https://github.com/byrman). * 👷 Add CI for Python 3.10. PR [#305](https://github.com/tiangolo/sqlmodel/pull/305) by [@tiangolo](https://github.com/tiangolo). * 📝 Add Jina's QA Bot to the docs to help people that want to ask quick questions. PR [#263](https://github.com/tiangolo/sqlmodel/pull/263) by [@tiangolo](https://github.com/tiangolo). From 0049436cd4dc86d0fd9a923dd00b5732fec1fd85 Mon Sep 17 00:00:00 2001 From: Ryan Grose Date: Sat, 27 Aug 2022 14:36:08 -0400 Subject: [PATCH 053/906] =?UTF-8?q?=E2=9C=8F=20Fix=20typo=20in=20`docs/tut?= =?UTF-8?q?orial/index.md`=20(#398)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/tutorial/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/index.md b/docs/tutorial/index.md index b45881138d..398cabafb4 100644 --- a/docs/tutorial/index.md +++ b/docs/tutorial/index.md @@ -2,7 +2,7 @@ ## Type hints -If you need a refreshed about how to use Python type hints (type annotations), check FastAPI's Python types intro. +If you need a refresher about how to use Python type hints (type annotations), check FastAPI's Python types intro. You can also check the mypy cheat sheet. From 296a0935d1c5f99a91b565a64d612902afaa1cab Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 18:36:49 +0000 Subject: [PATCH 054/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index a189538e29..0bec0f1b75 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typo in `docs/tutorial/index.md`. PR [#398](https://github.com/tiangolo/sqlmodel/pull/398) by [@ryangrose](https://github.com/ryangrose). * ⬆ Upgrade constrain for SQLAlchemy = ">=1.4.17,<=1.4.41". PR [#371](https://github.com/tiangolo/sqlmodel/pull/371) by [@RobertRosca](https://github.com/RobertRosca). * 🐛 Fix SQLAlchemy version 1.4.36 breaks SQLModel relationships (#315). PR [#322](https://github.com/tiangolo/sqlmodel/pull/322) by [@byrman](https://github.com/byrman). * 👷 Add CI for Python 3.10. PR [#305](https://github.com/tiangolo/sqlmodel/pull/305) by [@tiangolo](https://github.com/tiangolo). From f7d1bbe5b6044da25c61624ce083b2a83b9a7a89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 27 Aug 2022 20:39:37 +0200 Subject: [PATCH 055/906] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20Poetry?= =?UTF-8?q?=20to=20version=20`=3D=3D1.2.0b1`=20(#303)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 6 +----- .github/workflows/publish.yml | 6 +----- .github/workflows/test.yml | 6 +----- 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 18e35b308e..451290e3bd 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -37,13 +37,9 @@ jobs: key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-root-docs - name: Install poetry if: steps.cache.outputs.cache-hit != 'true' - # TODO: remove python -m pip install --force git+https://github.com/python-poetry/poetry-core.git@ad33bc2 - # once there's a release of Poetry 1.2.x including poetry-core > 1.1.0a6 - # Ref: https://github.com/python-poetry/poetry-core/pull/188 run: | python -m pip install --upgrade pip - python -m pip install --force git+https://github.com/python-poetry/poetry-core.git@ad33bc2 - python -m pip install "poetry==1.2.0a2" + python -m pip install "poetry==1.2.0b1" python -m poetry plugin add poetry-version-plugin - name: Configure poetry run: python -m poetry config virtualenvs.create false diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 105dbdd4cc..193f5ce0b3 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -33,13 +33,9 @@ jobs: key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-root - name: Install poetry if: steps.cache.outputs.cache-hit != 'true' - # TODO: remove python -m pip install --force git+https://github.com/python-poetry/poetry-core.git@ad33bc2 - # once there's a release of Poetry 1.2.x including poetry-core > 1.1.0a6 - # Ref: https://github.com/python-poetry/poetry-core/pull/188 run: | python -m pip install --upgrade pip - python -m pip install --force git+https://github.com/python-poetry/poetry-core.git@ad33bc2 - python -m pip install "poetry==1.2.0a2" + python -m pip install "poetry==1.2.0b1" python -m poetry plugin add poetry-version-plugin - name: Configure poetry run: python -m poetry config virtualenvs.create false diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c135ff3915..2ddd49923b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,13 +40,9 @@ jobs: key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-root - name: Install poetry if: steps.cache.outputs.cache-hit != 'true' - # TODO: remove python -m pip install --force git+https://github.com/python-poetry/poetry-core.git@ad33bc2 - # once there's a release of Poetry 1.2.x including poetry-core > 1.1.0a6 - # Ref: https://github.com/python-poetry/poetry-core/pull/188 run: | python -m pip install --upgrade pip - python -m pip install --force git+https://github.com/python-poetry/poetry-core.git@ad33bc2 - python -m pip install "poetry==1.2.0a2" + python -m pip install "poetry==1.2.0b1" python -m poetry plugin add poetry-version-plugin - name: Configure poetry run: python -m poetry config virtualenvs.create false From aca18da21e36681cc6d791086ab0fd9684660be5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 27 Aug 2022 20:39:53 +0200 Subject: [PATCH 056/906] =?UTF-8?q?=F0=9F=91=B7=20Add=20dependabot=20for?= =?UTF-8?q?=20GitHub=20Actions=20(#410)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b38df29f46..946f2358c0 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,5 +1,11 @@ version: 2 updates: + # GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + # Python - package-ecosystem: "pip" directory: "/" schedule: From dc0ecbb2c2d447127020c080b44154502efff13d Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 18:40:20 +0000 Subject: [PATCH 057/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 0bec0f1b75..d1ef1fbcc8 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆️ Upgrade Poetry to version `==1.2.0b1`. PR [#303](https://github.com/tiangolo/sqlmodel/pull/303) by [@tiangolo](https://github.com/tiangolo). * ✏ Fix typo in `docs/tutorial/index.md`. PR [#398](https://github.com/tiangolo/sqlmodel/pull/398) by [@ryangrose](https://github.com/ryangrose). * ⬆ Upgrade constrain for SQLAlchemy = ">=1.4.17,<=1.4.41". PR [#371](https://github.com/tiangolo/sqlmodel/pull/371) by [@RobertRosca](https://github.com/RobertRosca). * 🐛 Fix SQLAlchemy version 1.4.36 breaks SQLModel relationships (#315). PR [#322](https://github.com/tiangolo/sqlmodel/pull/322) by [@byrman](https://github.com/byrman). From 36b0c1ba082830e067d28d824316bddb4858647d Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 18:40:28 +0000 Subject: [PATCH 058/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index d1ef1fbcc8..ce8e9ffcde 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Add dependabot for GitHub Actions. PR [#410](https://github.com/tiangolo/sqlmodel/pull/410) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade Poetry to version `==1.2.0b1`. PR [#303](https://github.com/tiangolo/sqlmodel/pull/303) by [@tiangolo](https://github.com/tiangolo). * ✏ Fix typo in `docs/tutorial/index.md`. PR [#398](https://github.com/tiangolo/sqlmodel/pull/398) by [@ryangrose](https://github.com/ryangrose). * ⬆ Upgrade constrain for SQLAlchemy = ">=1.4.17,<=1.4.41". PR [#371](https://github.com/tiangolo/sqlmodel/pull/371) by [@RobertRosca](https://github.com/RobertRosca). From bc6dc0bafcea92125305bb90aa74197a2ff1a1c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 27 Aug 2022 22:08:25 +0200 Subject: [PATCH 059/906] =?UTF-8?q?=E2=8F=AA=20Revert=20upgrade=20Poetry,?= =?UTF-8?q?=20to=20make=20a=20release=20that=20supports=20Python=203.6=20f?= =?UTF-8?q?irst=20(#417)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 6 +++++- .github/workflows/publish.yml | 6 +++++- .github/workflows/test.yml | 10 +++++++--- pyproject.toml | 2 ++ 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 451290e3bd..18e35b308e 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -37,9 +37,13 @@ jobs: key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-root-docs - name: Install poetry if: steps.cache.outputs.cache-hit != 'true' + # TODO: remove python -m pip install --force git+https://github.com/python-poetry/poetry-core.git@ad33bc2 + # once there's a release of Poetry 1.2.x including poetry-core > 1.1.0a6 + # Ref: https://github.com/python-poetry/poetry-core/pull/188 run: | python -m pip install --upgrade pip - python -m pip install "poetry==1.2.0b1" + python -m pip install --force git+https://github.com/python-poetry/poetry-core.git@ad33bc2 + python -m pip install "poetry==1.2.0a2" python -m poetry plugin add poetry-version-plugin - name: Configure poetry run: python -m poetry config virtualenvs.create false diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 193f5ce0b3..105dbdd4cc 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -33,9 +33,13 @@ jobs: key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-root - name: Install poetry if: steps.cache.outputs.cache-hit != 'true' + # TODO: remove python -m pip install --force git+https://github.com/python-poetry/poetry-core.git@ad33bc2 + # once there's a release of Poetry 1.2.x including poetry-core > 1.1.0a6 + # Ref: https://github.com/python-poetry/poetry-core/pull/188 run: | python -m pip install --upgrade pip - python -m pip install "poetry==1.2.0b1" + python -m pip install --force git+https://github.com/python-poetry/poetry-core.git@ad33bc2 + python -m pip install "poetry==1.2.0a2" python -m poetry plugin add poetry-version-plugin - name: Configure poetry run: python -m poetry config virtualenvs.create false diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2ddd49923b..0d32926218 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["3.6.15", "3.7", "3.8", "3.9", "3.10"] fail-fast: false steps: @@ -40,9 +40,13 @@ jobs: key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-root - name: Install poetry if: steps.cache.outputs.cache-hit != 'true' + # TODO: remove python -m pip install --force git+https://github.com/python-poetry/poetry-core.git@ad33bc2 + # once there's a release of Poetry 1.2.x including poetry-core > 1.1.0a6 + # Ref: https://github.com/python-poetry/poetry-core/pull/188 run: | python -m pip install --upgrade pip - python -m pip install "poetry==1.2.0b1" + python -m pip install --force git+https://github.com/python-poetry/poetry-core.git@ad33bc2 + python -m pip install "poetry==1.2.0a2" python -m poetry plugin add poetry-version-plugin - name: Configure poetry run: python -m poetry config virtualenvs.create false @@ -50,7 +54,7 @@ jobs: if: steps.cache.outputs.cache-hit != 'true' run: python -m poetry install - name: Lint - if: ${{ matrix.python-version != '3.6' }} + if: ${{ matrix.python-version != '3.6.15' }} run: python -m poetry run bash scripts/lint.sh - name: Test run: python -m poetry run bash scripts/test.sh diff --git a/pyproject.toml b/pyproject.toml index d2ecfb785d..3c286fd19b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,3 +102,5 @@ strict_equality = true [[tool.mypy.overrides]] module = "sqlmodel.sql.expression" warn_unused_ignores = false + +# invalidate CI cache: 1 From db29f532951e5c48240f498a35de1c12430b0746 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 20:09:33 +0000 Subject: [PATCH 060/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index ce8e9ffcde..081c98957e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⏪ Revert upgrade Poetry, to make a release that supports Python 3.6 first. PR [#417](https://github.com/tiangolo/sqlmodel/pull/417) by [@tiangolo](https://github.com/tiangolo). * 👷 Add dependabot for GitHub Actions. PR [#410](https://github.com/tiangolo/sqlmodel/pull/410) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade Poetry to version `==1.2.0b1`. PR [#303](https://github.com/tiangolo/sqlmodel/pull/303) by [@tiangolo](https://github.com/tiangolo). * ✏ Fix typo in `docs/tutorial/index.md`. PR [#398](https://github.com/tiangolo/sqlmodel/pull/398) by [@ryangrose](https://github.com/ryangrose). From dc4dc42ec5732e0467ae6f8df449998462ef242e Mon Sep 17 00:00:00 2001 From: Jakob Jul Elben Date: Sat, 27 Aug 2022 22:13:32 +0200 Subject: [PATCH 061/906] =?UTF-8?q?=E2=9C=A8=20Raise=20an=20exception=20wh?= =?UTF-8?q?en=20using=20a=20Pydantic=20field=20type=20with=20no=20matching?= =?UTF-8?q?=20SQLAlchemy=20type=20(#18)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- sqlmodel/main.py | 1 + tests/test_missing_type.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 tests/test_missing_type.py diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 63c6dcbe5f..9efdafeca3 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -415,6 +415,7 @@ def get_sqlachemy_type(field: ModelField) -> Any: return AutoString if issubclass(field.type_, uuid.UUID): return GUID + raise ValueError(f"The field {field.name} has no matching SQLAlchemy type") def get_column_from_field(field: ModelField) -> Column: # type: ignore diff --git a/tests/test_missing_type.py b/tests/test_missing_type.py new file mode 100644 index 0000000000..2185fa43e9 --- /dev/null +++ b/tests/test_missing_type.py @@ -0,0 +1,21 @@ +from typing import Optional + +import pytest +from sqlmodel import Field, SQLModel + + +def test_missing_sql_type(): + class CustomType: + @classmethod + def __get_validators__(cls): + yield cls.validate + + @classmethod + def validate(cls, v): + return v + + with pytest.raises(ValueError): + + class Item(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + item: CustomType From 6da8dcfc8ec0fb51db9cf2950e8c2e68464ca34e Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 20:14:11 +0000 Subject: [PATCH 062/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 081c98957e..7f1b407cbb 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Raise an exception when using a Pydantic field type with no matching SQLAlchemy type. PR [#18](https://github.com/tiangolo/sqlmodel/pull/18) by [@elben10](https://github.com/elben10). * ⏪ Revert upgrade Poetry, to make a release that supports Python 3.6 first. PR [#417](https://github.com/tiangolo/sqlmodel/pull/417) by [@tiangolo](https://github.com/tiangolo). * 👷 Add dependabot for GitHub Actions. PR [#410](https://github.com/tiangolo/sqlmodel/pull/410) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade Poetry to version `==1.2.0b1`. PR [#303](https://github.com/tiangolo/sqlmodel/pull/303) by [@tiangolo](https://github.com/tiangolo). From 0197c6e211a55613ccbae8eb02a87f2ef872feb9 Mon Sep 17 00:00:00 2001 From: xginn8 Date: Sat, 27 Aug 2022 16:14:23 -0400 Subject: [PATCH 063/906] =?UTF-8?q?=E2=9C=8F=20Fix=20typos=20in=20`docs/tu?= =?UTF-8?q?torial/many-to-many/create-models-with-link.md`=20(#45)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- .../tutorial/many-to-many/create-models-with-link.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/tutorial/many-to-many/create-models-with-link.md b/docs/tutorial/many-to-many/create-models-with-link.md index 2b5fb8cf73..bc4481f73d 100644 --- a/docs/tutorial/many-to-many/create-models-with-link.md +++ b/docs/tutorial/many-to-many/create-models-with-link.md @@ -40,7 +40,7 @@ And **both fields are primary keys**. We hadn't used this before. 🤓 Let's see the `Team` model, it's almost identical as before, but with a little change: ```Python hl_lines="8" -# Code above ommited 👆 +# Code above omitted 👆 {!./docs_src/tutorial/many_to_many/tutorial001.py[ln:15-20]!} @@ -56,7 +56,7 @@ Let's see the `Team` model, it's almost identical as before, but with a little c -The **relationship attribute `heroes`** is still a list of heroes, annotatted as `List["Hero"]`. Again, we use `"Hero"` in quotes because we haven't declared that class yet by this point in the code (but as you know, editors and **SQLModel** understand that). +The **relationship attribute `heroes`** is still a list of heroes, annotated as `List["Hero"]`. Again, we use `"Hero"` in quotes because we haven't declared that class yet by this point in the code (but as you know, editors and **SQLModel** understand that). We use the same **`Relationship()`** function. @@ -69,7 +69,7 @@ And here's the important part to allow the **many-to-many** relationship, we use Let's see the other side, here's the `Hero` model: ```Python hl_lines="9" -# Code above ommited 👆 +# Code above omitted 👆 {!./docs_src/tutorial/many_to_many/tutorial001.py[ln:23-29]!} @@ -102,7 +102,7 @@ And now we have a **`link_model=HeroTeamLink`**. ✨ The same as before, we will have the rest of the code to create the **engine**, and a function to create all the tables `create_db_and_tables()`. ```Python hl_lines="9" -# Code above ommited 👆 +# Code above omitted 👆 {!./docs_src/tutorial/many_to_many/tutorial001.py[ln:32-39]!} @@ -122,7 +122,7 @@ The same as before, we will have the rest of the code to create the **engine**, And as in previous examples, we will add that function to a function `main()`, and we will call that `main()` function in the main block: ```Python hl_lines="4" -# Code above ommited 👆 +# Code above omitted 👆 {!./docs_src/tutorial/many_to_many/tutorial001.py[ln:78-79]!} # We will do more stuff here later 👈 @@ -149,7 +149,7 @@ If you run the code in the command line, it would output: ```console $ python app.py -// Boilerplate ommited 😉 +// Boilerplate omitted 😉 INFO Engine CREATE TABLE team ( From 4a08ee89ee9c62a2f9181a78a7d7cb901515776a Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 20:15:17 +0000 Subject: [PATCH 064/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 7f1b407cbb..5d5006339e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typos in `docs/tutorial/many-to-many/create-models-with-link.md`. PR [#45](https://github.com/tiangolo/sqlmodel/pull/45) by [@xginn8](https://github.com/xginn8). * ✨ Raise an exception when using a Pydantic field type with no matching SQLAlchemy type. PR [#18](https://github.com/tiangolo/sqlmodel/pull/18) by [@elben10](https://github.com/elben10). * ⏪ Revert upgrade Poetry, to make a release that supports Python 3.6 first. PR [#417](https://github.com/tiangolo/sqlmodel/pull/417) by [@tiangolo](https://github.com/tiangolo). * 👷 Add dependabot for GitHub Actions. PR [#410](https://github.com/tiangolo/sqlmodel/pull/410) by [@tiangolo](https://github.com/tiangolo). From 7bb99f2bd5e551168fab496611d28eb4d216c47c Mon Sep 17 00:00:00 2001 From: Brent <20882097+alucarddelta@users.noreply.github.com> Date: Sun, 28 Aug 2022 06:20:05 +1000 Subject: [PATCH 065/906] =?UTF-8?q?=E2=AC=86=20Update=20development=20requ?= =?UTF-8?q?irement=20for=20FastAPI=20from=20`^0.68.0`=20to=20`^0.68.1`=20(?= =?UTF-8?q?#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3c286fd19b..7f5e7f8037 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ mkdocs = "^1.2.1" mkdocs-material = "^8.1.4" mdx-include = "^1.4.1" coverage = {extras = ["toml"], version = "^5.5"} -fastapi = "^0.68.0" +fastapi = "^0.68.1" requests = "^2.26.0" autoflake = "^1.4" isort = "^5.9.3" From 9664c8814c91bfe0de340414c6f5881033b060c4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 20:20:44 +0000 Subject: [PATCH 066/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 5d5006339e..11cab0ae51 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Update development requirement for FastAPI from `^0.68.0` to `^0.68.1`. PR [#48](https://github.com/tiangolo/sqlmodel/pull/48) by [@alucarddelta](https://github.com/alucarddelta). * ✏ Fix typos in `docs/tutorial/many-to-many/create-models-with-link.md`. PR [#45](https://github.com/tiangolo/sqlmodel/pull/45) by [@xginn8](https://github.com/xginn8). * ✨ Raise an exception when using a Pydantic field type with no matching SQLAlchemy type. PR [#18](https://github.com/tiangolo/sqlmodel/pull/18) by [@elben10](https://github.com/elben10). * ⏪ Revert upgrade Poetry, to make a release that supports Python 3.6 first. PR [#417](https://github.com/tiangolo/sqlmodel/pull/417) by [@tiangolo](https://github.com/tiangolo). From 31beaf10170d6dd2ec07d8fcff12da3e1dc3950a Mon Sep 17 00:00:00 2001 From: mborus Date: Sat, 27 Aug 2022 22:30:59 +0200 Subject: [PATCH 067/906] =?UTF-8?q?=E2=9C=8F=20Fix=20broken=20link=20to=20?= =?UTF-8?q?newsletter=20sign-up=20in=20`docs/help.md`=20(#84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/help.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/help.md b/docs/help.md index bf2360bd60..6cde4c6142 100644 --- a/docs/help.md +++ b/docs/help.md @@ -12,7 +12,7 @@ And there are several ways to get help too. ## Subscribe to the FastAPI and Friends newsletter -You can subscribe to the (infrequent) [**FastAPI and friends** newsletter](/newsletter/){.internal-link target=_blank} to stay updated about: +You can subscribe to the (infrequent) **FastAPI and friends** newsletter to stay updated about: * News about FastAPI and friends, including SQLModel 🚀 * Guides 📝 From 943892ddb24aec0501726225b9117cbaa75b1c3a Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 20:31:46 +0000 Subject: [PATCH 068/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 11cab0ae51..2c9f126151 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix broken link to newsletter sign-up in `docs/help.md`. PR [#84](https://github.com/tiangolo/sqlmodel/pull/84) by [@mborus](https://github.com/mborus). * ⬆ Update development requirement for FastAPI from `^0.68.0` to `^0.68.1`. PR [#48](https://github.com/tiangolo/sqlmodel/pull/48) by [@alucarddelta](https://github.com/alucarddelta). * ✏ Fix typos in `docs/tutorial/many-to-many/create-models-with-link.md`. PR [#45](https://github.com/tiangolo/sqlmodel/pull/45) by [@xginn8](https://github.com/xginn8). * ✨ Raise an exception when using a Pydantic field type with no matching SQLAlchemy type. PR [#18](https://github.com/tiangolo/sqlmodel/pull/18) by [@elben10](https://github.com/elben10). From 5dff4d15e8fac9531503c85fbcbefd32f8b42e31 Mon Sep 17 00:00:00 2001 From: Dhiraj Gupta Date: Sat, 27 Aug 2022 16:32:02 -0400 Subject: [PATCH 069/906] =?UTF-8?q?=E2=9C=8F=20Fix=20typo=20in=20`docs/tut?= =?UTF-8?q?orial/code-structure.md`=20(#91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/tutorial/code-structure.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/code-structure.md b/docs/tutorial/code-structure.md index 0d91b4d5f5..31698a48b5 100644 --- a/docs/tutorial/code-structure.md +++ b/docs/tutorial/code-structure.md @@ -8,7 +8,7 @@ The class `Hero` has a reference to the class `Team` internally. But the class `Team` also has a reference to the class `Hero`. -So, if those two classes where in separate files and you tried to import the classes in each other's file directly, it would result in a **circular import**. 🔄 +So, if those two classes were in separate files and you tried to import the classes in each other's file directly, it would result in a **circular import**. 🔄 And Python will not be able to handle it and will throw an error. 🚨 From f67a13a5fb5ba7f1ee96a80438022a2196bf5ce7 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 20:32:46 +0000 Subject: [PATCH 070/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 2c9f126151..d12a27adb4 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typo in `docs/tutorial/code-structure.md`. PR [#91](https://github.com/tiangolo/sqlmodel/pull/91) by [@dhiraj](https://github.com/dhiraj). * ✏ Fix broken link to newsletter sign-up in `docs/help.md`. PR [#84](https://github.com/tiangolo/sqlmodel/pull/84) by [@mborus](https://github.com/mborus). * ⬆ Update development requirement for FastAPI from `^0.68.0` to `^0.68.1`. PR [#48](https://github.com/tiangolo/sqlmodel/pull/48) by [@alucarddelta](https://github.com/alucarddelta). * ✏ Fix typos in `docs/tutorial/many-to-many/create-models-with-link.md`. PR [#45](https://github.com/tiangolo/sqlmodel/pull/45) by [@xginn8](https://github.com/xginn8). From f3063a8e16d30c1e3d2e1966f12437707cae8511 Mon Sep 17 00:00:00 2001 From: Fedor Kuznetsov <55871784+ZettZet@users.noreply.github.com> Date: Sun, 28 Aug 2022 01:32:58 +0500 Subject: [PATCH 071/906] =?UTF-8?q?=E2=9C=8F=20Fix=20typo=20in=20`docs/tut?= =?UTF-8?q?orial/where.md`=20(#72)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez From f602794f0717eff568f0679ec8cde607e4e6f2cd Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 20:33:32 +0000 Subject: [PATCH 072/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index d12a27adb4..eafc3d429a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typo in `docs/tutorial/where.md`. PR [#72](https://github.com/tiangolo/sqlmodel/pull/72) by [@ZettZet](https://github.com/ZettZet). * ✏ Fix typo in `docs/tutorial/code-structure.md`. PR [#91](https://github.com/tiangolo/sqlmodel/pull/91) by [@dhiraj](https://github.com/dhiraj). * ✏ Fix broken link to newsletter sign-up in `docs/help.md`. PR [#84](https://github.com/tiangolo/sqlmodel/pull/84) by [@mborus](https://github.com/mborus). * ⬆ Update development requirement for FastAPI from `^0.68.0` to `^0.68.1`. PR [#48](https://github.com/tiangolo/sqlmodel/pull/48) by [@alucarddelta](https://github.com/alucarddelta). From 63dd44dc86b00fdc0b7f348e9f5650bbf6557173 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Sat, 27 Aug 2022 16:33:41 -0400 Subject: [PATCH 073/906] =?UTF-8?q?=E2=9C=8F=20Fix=20typo=20in=20`docs/tut?= =?UTF-8?q?orial/fastapi/tests.md`=20(#113)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/tutorial/fastapi/tests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/fastapi/tests.md b/docs/tutorial/fastapi/tests.md index eaf3ef380f..15ebc84328 100644 --- a/docs/tutorial/fastapi/tests.md +++ b/docs/tutorial/fastapi/tests.md @@ -311,7 +311,7 @@ Let's add some more tests: That's why we add these two extra tests here. -Now, any additional test functions can be as **simple** as the first one, they just have to **declate the `client` parameter** to get the `TestClient` **fixture** with all the database stuff setup. Nice! 😎 +Now, any additional test functions can be as **simple** as the first one, they just have to **declare the `client` parameter** to get the `TestClient` **fixture** with all the database stuff setup. Nice! 😎 ## Why Two Fixtures From 006cf488e84d415a8d1e89e6f6f1a7e209462be1 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 20:34:14 +0000 Subject: [PATCH 074/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index eafc3d429a..98ac55aabe 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typo in `docs/tutorial/fastapi/tests.md`. PR [#113](https://github.com/tiangolo/sqlmodel/pull/113) by [@feanil](https://github.com/feanil). * ✏ Fix typo in `docs/tutorial/where.md`. PR [#72](https://github.com/tiangolo/sqlmodel/pull/72) by [@ZettZet](https://github.com/ZettZet). * ✏ Fix typo in `docs/tutorial/code-structure.md`. PR [#91](https://github.com/tiangolo/sqlmodel/pull/91) by [@dhiraj](https://github.com/dhiraj). * ✏ Fix broken link to newsletter sign-up in `docs/help.md`. PR [#84](https://github.com/tiangolo/sqlmodel/pull/84) by [@mborus](https://github.com/mborus). From d032c3cfea5ce799a637ea9d40b5f5209a3330c1 Mon Sep 17 00:00:00 2001 From: Saman Nezafat <77416478+onionj@users.noreply.github.com> Date: Sun, 28 Aug 2022 01:10:57 +0430 Subject: [PATCH 075/906] =?UTF-8?q?=E2=9C=8F=20Fix=20typo=20variable=20in?= =?UTF-8?q?=20example=20about=20relationships=20and=20`back=5Fpopulates`,?= =?UTF-8?q?=20always=20use=20`hero`=20instead=20of=20`owner`=20(#120)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- .../relationship_attributes/back_populates/tutorial003.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py b/docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py index c137f58f6a..98e197002e 100644 --- a/docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py +++ b/docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py @@ -36,7 +36,7 @@ class Hero(SQLModel, table=True): team: Optional[Team] = Relationship(back_populates="heroes") weapon_id: Optional[int] = Field(default=None, foreign_key="weapon.id") - weapon: Optional[Weapon] = Relationship(back_populates="owner") + weapon: Optional[Weapon] = Relationship(back_populates="hero") powers: List[Power] = Relationship(back_populates="hero") From acc27dabc925b4dc6b45634b7b05e1515ba6b57a Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 20:41:29 +0000 Subject: [PATCH 076/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 98ac55aabe..fe2d08da34 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typo variable in example about relationships and `back_populates`, always use `hero` instead of `owner`. PR [#120](https://github.com/tiangolo/sqlmodel/pull/120) by [@onionj](https://github.com/onionj). * ✏ Fix typo in `docs/tutorial/fastapi/tests.md`. PR [#113](https://github.com/tiangolo/sqlmodel/pull/113) by [@feanil](https://github.com/feanil). * ✏ Fix typo in `docs/tutorial/where.md`. PR [#72](https://github.com/tiangolo/sqlmodel/pull/72) by [@ZettZet](https://github.com/ZettZet). * ✏ Fix typo in `docs/tutorial/code-structure.md`. PR [#91](https://github.com/tiangolo/sqlmodel/pull/91) by [@dhiraj](https://github.com/dhiraj). From 184c8eb5a938fd2fa22ec9dc667c075e51c64adc Mon Sep 17 00:00:00 2001 From: Chris Goddard Date: Sat, 27 Aug 2022 13:48:09 -0700 Subject: [PATCH 077/906] =?UTF-8?q?=E2=9C=8F=20Fix=20typo=20in=20`docs/tut?= =?UTF-8?q?orial/fastapi/teams.md`=20(#154)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez From 13544c0f44c3ce380e97f638d3e64ae137e56295 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 20:48:52 +0000 Subject: [PATCH 078/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index fe2d08da34..6eb0a820bd 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typo in `docs/tutorial/fastapi/teams.md`. PR [#154](https://github.com/tiangolo/sqlmodel/pull/154) by [@chrisgoddard](https://github.com/chrisgoddard). * ✏ Fix typo variable in example about relationships and `back_populates`, always use `hero` instead of `owner`. PR [#120](https://github.com/tiangolo/sqlmodel/pull/120) by [@onionj](https://github.com/onionj). * ✏ Fix typo in `docs/tutorial/fastapi/tests.md`. PR [#113](https://github.com/tiangolo/sqlmodel/pull/113) by [@feanil](https://github.com/feanil). * ✏ Fix typo in `docs/tutorial/where.md`. PR [#72](https://github.com/tiangolo/sqlmodel/pull/72) by [@ZettZet](https://github.com/ZettZet). From 6f1ffccd4f7a32d61e97290b58f237c28c496b89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5ns=20Magnusson?= Date: Sat, 27 Aug 2022 22:50:33 +0200 Subject: [PATCH 079/906] =?UTF-8?q?=E2=9C=8F=20Fix=20typos=20in=20`docs/tu?= =?UTF-8?q?torial/code-structure.md`,=20`docs/tutorial/fastapi/multiple-mo?= =?UTF-8?q?dels.md`,=20`docs/tutorial/fastapi/simple-hero-api.md`,=20`docs?= =?UTF-8?q?/tutorial/many-to-many/index.md`=20(#116)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: moonso Co-authored-by: Sebastián Ramírez --- docs/tutorial/code-structure.md | 2 +- docs/tutorial/fastapi/multiple-models.md | 2 +- docs/tutorial/many-to-many/index.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/tutorial/code-structure.md b/docs/tutorial/code-structure.md index 31698a48b5..f46dc1e4c9 100644 --- a/docs/tutorial/code-structure.md +++ b/docs/tutorial/code-structure.md @@ -170,7 +170,7 @@ Let's assume that now the file structure is: The problem with circular imports is that Python can't resolve them at *runtime*. -but when using Python **type annotations** it's very common to need to declare the type of some variables with classes imported from other files. +But when using Python **type annotations** it's very common to need to declare the type of some variables with classes imported from other files. And the files with those classes might **also need to import** more things from the first files. diff --git a/docs/tutorial/fastapi/multiple-models.md b/docs/tutorial/fastapi/multiple-models.md index d313874c98..3643ec8fcc 100644 --- a/docs/tutorial/fastapi/multiple-models.md +++ b/docs/tutorial/fastapi/multiple-models.md @@ -361,7 +361,7 @@ And because we can't leave the empty space when creating a new class, but we don This means that there's nothing else special in this class apart from the fact that it is named `HeroCreate` and that it inherits from `HeroBase`. -As an alternative, we could use `HeroBase` directly in the API code instead of `HeroCreate`, but it would show up in the auomatic docs UI with that name "`HeroBase`" which could be **confusing** for clients. Instead, "`HeroCreate`" is a bit more explicit about what it is for. +As an alternative, we could use `HeroBase` directly in the API code instead of `HeroCreate`, but it would show up in the automatic docs UI with that name "`HeroBase`" which could be **confusing** for clients. Instead, "`HeroCreate`" is a bit more explicit about what it is for. On top of that, we could easily decide in the future that we want to receive **more data** when creating a new hero apart from the data in `HeroBase` (for example a password), and now we already have the class to put those extra fields. diff --git a/docs/tutorial/many-to-many/index.md b/docs/tutorial/many-to-many/index.md index 24d7824fe0..e2e34777c0 100644 --- a/docs/tutorial/many-to-many/index.md +++ b/docs/tutorial/many-to-many/index.md @@ -60,7 +60,7 @@ Notice that each hero can only have **one** connection. But each team can receiv ## Introduce Many-to-Many -But let's say that as **Deadpond** is a great chracter, they recruit him to the new **Preventers** team, but he's still part of the **Z-Force** team too. +But let's say that as **Deadpond** is a great character, they recruit him to the new **Preventers** team, but he's still part of the **Z-Force** team too. So, now, we need to be able to have a hero that is connected to **many** teams. And then, each team, should still be able to receive **many** heroes. So we need a **Many-to-Many** relationship. From 8bee55e23bebd3619d3e9cdd0efa7950d92b2bf4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 20:51:10 +0000 Subject: [PATCH 080/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 6eb0a820bd..054814e4c7 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typos in `docs/tutorial/code-structure.md`, `docs/tutorial/fastapi/multiple-models.md`, `docs/tutorial/fastapi/simple-hero-api.md`, `docs/tutorial/many-to-many/index.md`. PR [#116](https://github.com/tiangolo/sqlmodel/pull/116) by [@moonso](https://github.com/moonso). * ✏ Fix typo in `docs/tutorial/fastapi/teams.md`. PR [#154](https://github.com/tiangolo/sqlmodel/pull/154) by [@chrisgoddard](https://github.com/chrisgoddard). * ✏ Fix typo variable in example about relationships and `back_populates`, always use `hero` instead of `owner`. PR [#120](https://github.com/tiangolo/sqlmodel/pull/120) by [@onionj](https://github.com/onionj). * ✏ Fix typo in `docs/tutorial/fastapi/tests.md`. PR [#113](https://github.com/tiangolo/sqlmodel/pull/113) by [@feanil](https://github.com/feanil). From aa5803fbbb40d2d1ab7d1fbf451453134421d4e9 Mon Sep 17 00:00:00 2001 From: wmcgee3 <61711986+wmcgee3@users.noreply.github.com> Date: Sat, 27 Aug 2022 16:51:46 -0400 Subject: [PATCH 081/906] =?UTF-8?q?=E2=9C=8F=20Fix=20typos=20in=20`docs/tu?= =?UTF-8?q?torial/fastapi/update.md`=20(#162)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pwildenhain <35195136+pwildenhain@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- docs/tutorial/fastapi/update.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/fastapi/update.md b/docs/tutorial/fastapi/update.md index b32e58281d..5620d3f230 100644 --- a/docs/tutorial/fastapi/update.md +++ b/docs/tutorial/fastapi/update.md @@ -222,7 +222,7 @@ So, we would use that value and upate the `age` to `None` in the database, **jus Notice that `age` here is `None`, and **we still detected it**. -Also that `name` was not even sent, and we don't *accidentaly* set it to `None` or something, we just didn't touch it, because the client didn't sent it, so we are **pefectly fine**, even in these corner cases. ✨ +Also that `name` was not even sent, and we don't *accidentally* set it to `None` or something, we just didn't touch it, because the client didn't send it, so we are **perfectly fine**, even in these corner cases. ✨ These are some of the advantages of Pydantic, that we can use with SQLModel. 🎉 From 48ada0cd5df26fc58852f82da12ba2a6aa63a064 Mon Sep 17 00:00:00 2001 From: Sean Eulenberg Date: Sat, 27 Aug 2022 22:52:24 +0200 Subject: [PATCH 082/906] =?UTF-8?q?=E2=9C=8F=20Fix=20typo=20in=20`docs/dat?= =?UTF-8?q?abases.md`=20(#177)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez From c0efc7b37067cabc914e92c45fc6162c74dbb529 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 20:52:33 +0000 Subject: [PATCH 083/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 054814e4c7..51db9da79e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typos in `docs/tutorial/fastapi/update.md`. PR [#162](https://github.com/tiangolo/sqlmodel/pull/162) by [@wmcgee3](https://github.com/wmcgee3). * ✏ Fix typos in `docs/tutorial/code-structure.md`, `docs/tutorial/fastapi/multiple-models.md`, `docs/tutorial/fastapi/simple-hero-api.md`, `docs/tutorial/many-to-many/index.md`. PR [#116](https://github.com/tiangolo/sqlmodel/pull/116) by [@moonso](https://github.com/moonso). * ✏ Fix typo in `docs/tutorial/fastapi/teams.md`. PR [#154](https://github.com/tiangolo/sqlmodel/pull/154) by [@chrisgoddard](https://github.com/chrisgoddard). * ✏ Fix typo variable in example about relationships and `back_populates`, always use `hero` instead of `owner`. PR [#120](https://github.com/tiangolo/sqlmodel/pull/120) by [@onionj](https://github.com/onionj). From 34e125357fa324bd5eb1c71a4528609b6f811be4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 20:53:09 +0000 Subject: [PATCH 084/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 51db9da79e..c595e625ff 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typo in `docs/databases.md`. PR [#177](https://github.com/tiangolo/sqlmodel/pull/177) by [@seandlg](https://github.com/seandlg). * ✏ Fix typos in `docs/tutorial/fastapi/update.md`. PR [#162](https://github.com/tiangolo/sqlmodel/pull/162) by [@wmcgee3](https://github.com/wmcgee3). * ✏ Fix typos in `docs/tutorial/code-structure.md`, `docs/tutorial/fastapi/multiple-models.md`, `docs/tutorial/fastapi/simple-hero-api.md`, `docs/tutorial/many-to-many/index.md`. PR [#116](https://github.com/tiangolo/sqlmodel/pull/116) by [@moonso](https://github.com/moonso). * ✏ Fix typo in `docs/tutorial/fastapi/teams.md`. PR [#154](https://github.com/tiangolo/sqlmodel/pull/154) by [@chrisgoddard](https://github.com/chrisgoddard). From 015f7acbc5a282897c34882f1785fb6026bdbf65 Mon Sep 17 00:00:00 2001 From: Gal Bracha Date: Sat, 27 Aug 2022 23:53:34 +0300 Subject: [PATCH 085/906] =?UTF-8?q?=E2=9C=8F=20Fix=20typos=20in=20`docs/tu?= =?UTF-8?q?torial/automatic-id-none-refresh.md`,=20`docs/tutorial/fastapi/?= =?UTF-8?q?update.md`,=20`docs/tutorial/select.md`=20(#185)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/tutorial/fastapi/update.md | 2 +- docs/tutorial/select.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/fastapi/update.md b/docs/tutorial/fastapi/update.md index 5620d3f230..e08f169bee 100644 --- a/docs/tutorial/fastapi/update.md +++ b/docs/tutorial/fastapi/update.md @@ -4,7 +4,7 @@ Now let's see how to update data in the database with a **FastAPI** *path operat ## `HeroUpdate` Model -We want clients to be able to udpate the `name`, the `secret_name`, and the `age` of a hero. +We want clients to be able to update the `name`, the `secret_name`, and the `age` of a hero. But we don't want them to have to include all the data again just to **update a single field**. diff --git a/docs/tutorial/select.md b/docs/tutorial/select.md index b5a092224f..fb638c1212 100644 --- a/docs/tutorial/select.md +++ b/docs/tutorial/select.md @@ -88,7 +88,7 @@ You can try that out in **DB Browser for SQLite**: ### A SQL Shortcut -If we want to get all the columns like in this case above, in SQL there's a shortcut, instead of specifying each of the column names wew could write a `*`: +If we want to get all the columns like in this case above, in SQL there's a shortcut, instead of specifying each of the column names we could write a `*`: ```SQL SELECT * From a5116a372cf0172cf4292877f5369dd656d48675 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 20:54:16 +0000 Subject: [PATCH 086/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index c595e625ff..7c545205ea 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typos in `docs/tutorial/automatic-id-none-refresh.md`, `docs/tutorial/fastapi/update.md`, `docs/tutorial/select.md`. PR [#185](https://github.com/tiangolo/sqlmodel/pull/185) by [@rootux](https://github.com/rootux). * ✏ Fix typo in `docs/databases.md`. PR [#177](https://github.com/tiangolo/sqlmodel/pull/177) by [@seandlg](https://github.com/seandlg). * ✏ Fix typos in `docs/tutorial/fastapi/update.md`. PR [#162](https://github.com/tiangolo/sqlmodel/pull/162) by [@wmcgee3](https://github.com/wmcgee3). * ✏ Fix typos in `docs/tutorial/code-structure.md`, `docs/tutorial/fastapi/multiple-models.md`, `docs/tutorial/fastapi/simple-hero-api.md`, `docs/tutorial/many-to-many/index.md`. PR [#116](https://github.com/tiangolo/sqlmodel/pull/116) by [@moonso](https://github.com/moonso). From 426da7c443a4b06128e2c46df564303848ab7622 Mon Sep 17 00:00:00 2001 From: Hao Wang Date: Sun, 28 Aug 2022 04:55:27 +0800 Subject: [PATCH 087/906] =?UTF-8?q?=E2=9C=8F=20Fix=20typo=20in=20`docs/tut?= =?UTF-8?q?orial/fastapi/simple-hero-api.md`=20(#247)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/tutorial/fastapi/simple-hero-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/fastapi/simple-hero-api.md b/docs/tutorial/fastapi/simple-hero-api.md index 8676136a46..5730186726 100644 --- a/docs/tutorial/fastapi/simple-hero-api.md +++ b/docs/tutorial/fastapi/simple-hero-api.md @@ -23,7 +23,7 @@ $ python -m pip install fastapi "uvicorn[standard]" ``` -s + ## **SQLModel** Code - Models, Engine Now let's start with the SQLModel code. From 04b8b3eedfe8e049c52e0c031f791867dc3f9df6 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 20:56:00 +0000 Subject: [PATCH 088/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 7c545205ea..79da850e6d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typo in `docs/tutorial/fastapi/simple-hero-api.md`. PR [#247](https://github.com/tiangolo/sqlmodel/pull/247) by [@hao-wang](https://github.com/hao-wang). * ✏ Fix typos in `docs/tutorial/automatic-id-none-refresh.md`, `docs/tutorial/fastapi/update.md`, `docs/tutorial/select.md`. PR [#185](https://github.com/tiangolo/sqlmodel/pull/185) by [@rootux](https://github.com/rootux). * ✏ Fix typo in `docs/databases.md`. PR [#177](https://github.com/tiangolo/sqlmodel/pull/177) by [@seandlg](https://github.com/seandlg). * ✏ Fix typos in `docs/tutorial/fastapi/update.md`. PR [#162](https://github.com/tiangolo/sqlmodel/pull/162) by [@wmcgee3](https://github.com/wmcgee3). From 452f18d8bc91e66ce43802b86b26b15a109bb9f7 Mon Sep 17 00:00:00 2001 From: cirrusj Date: Sun, 28 Aug 2022 00:00:09 +0300 Subject: [PATCH 089/906] =?UTF-8?q?=E2=9C=8F=20Fix=20typos=20in=20`docs/tu?= =?UTF-8?q?torial/fastapi/update.md`=20(#268)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/tutorial/fastapi/update.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/fastapi/update.md b/docs/tutorial/fastapi/update.md index e08f169bee..b845d5a22c 100644 --- a/docs/tutorial/fastapi/update.md +++ b/docs/tutorial/fastapi/update.md @@ -218,11 +218,11 @@ And when getting the data with `hero.dict(exclude_unset=True)`, we would get: } ``` -So, we would use that value and upate the `age` to `None` in the database, **just as the client intended**. +So, we would use that value and update the `age` to `None` in the database, **just as the client intended**. Notice that `age` here is `None`, and **we still detected it**. -Also that `name` was not even sent, and we don't *accidentally* set it to `None` or something, we just didn't touch it, because the client didn't send it, so we are **perfectly fine**, even in these corner cases. ✨ +Also that `name` was not even sent, and we don't *accidentally* set it to `None` or something, we just didn't touch it, because the client didn't sent it, so we are **perfectly fine**, even in these corner cases. ✨ These are some of the advantages of Pydantic, that we can use with SQLModel. 🎉 From e5fdc371f6c8e323e5cfd47b0c1860acb1764b74 Mon Sep 17 00:00:00 2001 From: Jorge Alvarado Date: Sat, 27 Aug 2022 17:00:53 -0400 Subject: [PATCH 090/906] =?UTF-8?q?=E2=9C=8F=20Fix=20typo=20in=20`docs/tut?= =?UTF-8?q?orial/where.md`=20(#286)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/tutorial/where.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/where.md b/docs/tutorial/where.md index 45e909cc75..d4e4639dba 100644 --- a/docs/tutorial/where.md +++ b/docs/tutorial/where.md @@ -865,7 +865,7 @@ It would be an error telling you that This is because as we are using pure and plain Python annotations for the fields, `age` is indeed annotated as `Optional[int]`, which means `int` or `None`. -By using this simple and standard Python type annotations We get the benefit of the extra simplicity and the inline error checks when creating or using instances. ✨ +By using this simple and standard Python type annotations we get the benefit of the extra simplicity and the inline error checks when creating or using instances. ✨ And when we use these special **class attributes** in a `.where()`, during execution of the program, the special class attribute will know that the comparison only applies for the values that are not `NULL` in the database, and it will work correctly. From 5f6b5bfd7f11d3ff0d8cf333cb018472012491d1 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 21:02:37 +0000 Subject: [PATCH 091/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 79da850e6d..9159d44971 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typos in `docs/tutorial/fastapi/update.md`. PR [#268](https://github.com/tiangolo/sqlmodel/pull/268) by [@cirrusj](https://github.com/cirrusj). * ✏ Fix typo in `docs/tutorial/fastapi/simple-hero-api.md`. PR [#247](https://github.com/tiangolo/sqlmodel/pull/247) by [@hao-wang](https://github.com/hao-wang). * ✏ Fix typos in `docs/tutorial/automatic-id-none-refresh.md`, `docs/tutorial/fastapi/update.md`, `docs/tutorial/select.md`. PR [#185](https://github.com/tiangolo/sqlmodel/pull/185) by [@rootux](https://github.com/rootux). * ✏ Fix typo in `docs/databases.md`. PR [#177](https://github.com/tiangolo/sqlmodel/pull/177) by [@seandlg](https://github.com/seandlg). From dc5876c7270f2f068e786ef03964d88577549792 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 21:02:59 +0000 Subject: [PATCH 092/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 9159d44971..bdc591b901 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typo in `docs/tutorial/where.md`. PR [#286](https://github.com/tiangolo/sqlmodel/pull/286) by [@jalvaradosegura](https://github.com/jalvaradosegura). * ✏ Fix typos in `docs/tutorial/fastapi/update.md`. PR [#268](https://github.com/tiangolo/sqlmodel/pull/268) by [@cirrusj](https://github.com/cirrusj). * ✏ Fix typo in `docs/tutorial/fastapi/simple-hero-api.md`. PR [#247](https://github.com/tiangolo/sqlmodel/pull/247) by [@hao-wang](https://github.com/hao-wang). * ✏ Fix typos in `docs/tutorial/automatic-id-none-refresh.md`, `docs/tutorial/fastapi/update.md`, `docs/tutorial/select.md`. PR [#185](https://github.com/tiangolo/sqlmodel/pull/185) by [@rootux](https://github.com/rootux). From 4de5a41720204fc0c67fb1a56a29b7ceb5023138 Mon Sep 17 00:00:00 2001 From: Jack Homan Date: Sat, 27 Aug 2022 17:04:38 -0400 Subject: [PATCH 093/906] =?UTF-8?q?=E2=9C=8F=20Fix=20typo=20in=20`docs/tut?= =?UTF-8?q?orial/fastapi/tests.md`=20(#265)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/tutorial/fastapi/tests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/fastapi/tests.md b/docs/tutorial/fastapi/tests.md index 15ebc84328..db71121f94 100644 --- a/docs/tutorial/fastapi/tests.md +++ b/docs/tutorial/fastapi/tests.md @@ -82,7 +82,7 @@ But now we need to deal with a bit of logistics and details we are not paying at This test looks fine, but there's a problem. -If we run it, it will use the same **production database** that we are using to store our very important **heroes**, and we will end up adding adding unnecesary data to it, or even worse, in future tests we could end up removing production data. +If we run it, it will use the same **production database** that we are using to store our very important **heroes**, and we will end up adding unnecesary data to it, or even worse, in future tests we could end up removing production data. So, we should use an independent **testing database**, just for the tests. From 6b433a0de4c0b5cd80b1a1a154332e96181ef212 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 21:05:21 +0000 Subject: [PATCH 094/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index bdc591b901..c28c6293bc 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typo in `docs/tutorial/fastapi/tests.md`. PR [#265](https://github.com/tiangolo/sqlmodel/pull/265) by [@johnhoman](https://github.com/johnhoman). * ✏ Fix typo in `docs/tutorial/where.md`. PR [#286](https://github.com/tiangolo/sqlmodel/pull/286) by [@jalvaradosegura](https://github.com/jalvaradosegura). * ✏ Fix typos in `docs/tutorial/fastapi/update.md`. PR [#268](https://github.com/tiangolo/sqlmodel/pull/268) by [@cirrusj](https://github.com/cirrusj). * ✏ Fix typo in `docs/tutorial/fastapi/simple-hero-api.md`. PR [#247](https://github.com/tiangolo/sqlmodel/pull/247) by [@hao-wang](https://github.com/hao-wang). From 106fb1fe9b5cdea58e2729765d417082ae67e53f Mon Sep 17 00:00:00 2001 From: Fardad13 <33404823+Fardad13@users.noreply.github.com> Date: Sat, 27 Aug 2022 23:06:15 +0200 Subject: [PATCH 095/906] =?UTF-8?q?=E2=9C=8F=20Fix=20typo=20in=20`docs/con?= =?UTF-8?q?tributing.md`=20(#323)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/contributing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contributing.md b/docs/contributing.md index 2cfa5331df..f2964fba9b 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -42,7 +42,7 @@ $ poetry shell -That will set up the environment variables needed dand will start a new shell with them. +That will set up the environment variables needed and start a new shell with them. #### Using your local SQLModel From 61294af824217b6268505154b62fb60a43382f13 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 21:06:58 +0000 Subject: [PATCH 096/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index c28c6293bc..e68e669d8a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typo in `docs/contributing.md`. PR [#323](https://github.com/tiangolo/sqlmodel/pull/323) by [@Fardad13](https://github.com/Fardad13). * ✏ Fix typo in `docs/tutorial/fastapi/tests.md`. PR [#265](https://github.com/tiangolo/sqlmodel/pull/265) by [@johnhoman](https://github.com/johnhoman). * ✏ Fix typo in `docs/tutorial/where.md`. PR [#286](https://github.com/tiangolo/sqlmodel/pull/286) by [@jalvaradosegura](https://github.com/jalvaradosegura). * ✏ Fix typos in `docs/tutorial/fastapi/update.md`. PR [#268](https://github.com/tiangolo/sqlmodel/pull/268) by [@cirrusj](https://github.com/cirrusj). From deed65095f19659d5878206e0b19d8497d1046ff Mon Sep 17 00:00:00 2001 From: gr8jam <23422130+gr8jam@users.noreply.github.com> Date: Sat, 27 Aug 2022 23:07:48 +0200 Subject: [PATCH 097/906] =?UTF-8?q?=E2=9C=8F=20Fix=20typo=20in=20`docs/db-?= =?UTF-8?q?to-code.md`=20(#155)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: gr8jam Co-authored-by: Sebastián Ramírez --- docs/db-to-code.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/db-to-code.md b/docs/db-to-code.md index d4b182d26e..ce9ffac251 100644 --- a/docs/db-to-code.md +++ b/docs/db-to-code.md @@ -143,7 +143,7 @@ If the user provides this ID: 2 ``` -...the would be this table (with a single row): +...the result would be this table (with a single row): From a993c2141d4d6650e37383be78f7431d46322a70 Mon Sep 17 00:00:00 2001 From: Marcio Mazza Date: Sat, 27 Aug 2022 18:08:20 -0300 Subject: [PATCH 098/906] =?UTF-8?q?=E2=9C=8F=20Fix=20typo=20in=20`docs/tut?= =?UTF-8?q?orial/code-structure.md`=20(#344)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/tutorial/code-structure.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/code-structure.md b/docs/tutorial/code-structure.md index f46dc1e4c9..59a9e4bd9a 100644 --- a/docs/tutorial/code-structure.md +++ b/docs/tutorial/code-structure.md @@ -198,7 +198,7 @@ It has a value of `True` for editors and tools that analyze the code with the ty But when Python is executing, its value is `False`. -So, we can us it in an `if` block and import things inside the `if` block. And they will be "imported" only for editors, but not at runtime. +So, we can use it in an `if` block and import things inside the `if` block. And they will be "imported" only for editors, but not at runtime. ### Hero Model File From bf153807331f66d7e80843ccdb77659b37522b8f Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 21:08:31 +0000 Subject: [PATCH 099/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index e68e669d8a..ec0b09475b 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typo in `docs/db-to-code.md`. PR [#155](https://github.com/tiangolo/sqlmodel/pull/155) by [@gr8jam](https://github.com/gr8jam). * ✏ Fix typo in `docs/contributing.md`. PR [#323](https://github.com/tiangolo/sqlmodel/pull/323) by [@Fardad13](https://github.com/Fardad13). * ✏ Fix typo in `docs/tutorial/fastapi/tests.md`. PR [#265](https://github.com/tiangolo/sqlmodel/pull/265) by [@johnhoman](https://github.com/johnhoman). * ✏ Fix typo in `docs/tutorial/where.md`. PR [#286](https://github.com/tiangolo/sqlmodel/pull/286) by [@jalvaradosegura](https://github.com/jalvaradosegura). From 1e69c00538e55921f340cda5b8779d67832e131f Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 21:09:00 +0000 Subject: [PATCH 100/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index ec0b09475b..db9d65520f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typo in `docs/tutorial/code-structure.md`. PR [#344](https://github.com/tiangolo/sqlmodel/pull/344) by [@marciomazza](https://github.com/marciomazza). * ✏ Fix typo in `docs/db-to-code.md`. PR [#155](https://github.com/tiangolo/sqlmodel/pull/155) by [@gr8jam](https://github.com/gr8jam). * ✏ Fix typo in `docs/contributing.md`. PR [#323](https://github.com/tiangolo/sqlmodel/pull/323) by [@Fardad13](https://github.com/Fardad13). * ✏ Fix typo in `docs/tutorial/fastapi/tests.md`. PR [#265](https://github.com/tiangolo/sqlmodel/pull/265) by [@johnhoman](https://github.com/johnhoman). From ad0766fe3ed40d8880af8fece48616d70d6809ff Mon Sep 17 00:00:00 2001 From: VictorGambarini Date: Sun, 28 Aug 2022 09:22:59 +1200 Subject: [PATCH 101/906] =?UTF-8?q?=E2=9C=8F=20Fix=20typos=20in=20multiple?= =?UTF-8?q?=20files=20in=20the=20docs=20(#400)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/tutorial/automatic-id-none-refresh.md | 18 +++++----- docs/tutorial/fastapi/delete.md | 2 +- docs/tutorial/fastapi/limit-and-offset.md | 10 +++--- docs/tutorial/fastapi/multiple-models.md | 36 +++++++++---------- docs/tutorial/fastapi/read-one.md | 2 +- docs/tutorial/fastapi/relationships.md | 16 ++++----- docs/tutorial/fastapi/response-model.md | 2 +- .../fastapi/session-with-dependency.md | 2 +- docs/tutorial/fastapi/simple-hero-api.md | 10 +++--- docs/tutorial/fastapi/teams.md | 6 ++-- docs/tutorial/fastapi/tests.md | 32 ++++++++--------- docs/tutorial/fastapi/update.md | 8 ++--- 12 files changed, 72 insertions(+), 72 deletions(-) diff --git a/docs/tutorial/automatic-id-none-refresh.md b/docs/tutorial/automatic-id-none-refresh.md index ed767a2121..ac6a2a4fca 100644 --- a/docs/tutorial/automatic-id-none-refresh.md +++ b/docs/tutorial/automatic-id-none-refresh.md @@ -1,6 +1,6 @@ # Automatic IDs, None Defaults, and Refreshing Data -In the previous chapter we saw how to add rows to the database using **SQLModel**. +In the previous chapter, we saw how to add rows to the database using **SQLModel**. Now let's talk a bit about why the `id` field **can't be `NULL`** on the database because it's a **primary key**, and we declare it using `Field(primary_key=True)`. @@ -11,7 +11,7 @@ But the same `id` field actually **can be `None`** in the Python code, so we dec {!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:6-10]!} -# Code below ommitted 👇 +# Code below omitted 👇 ```
@@ -68,7 +68,7 @@ If we ran this code before saving the hero to the database and the `hero_1.id` w TypeError: unsupported operand type(s) for +: 'NoneType' and 'int' ``` -But by declaring it with `Optional[int]` the editor will help us to avoid writing broken code by showing us a warning telling us that the code could be invalid if `hero_1.id` is `None`. 🔍 +But by declaring it with `Optional[int]`, the editor will help us to avoid writing broken code by showing us a warning telling us that the code could be invalid if `hero_1.id` is `None`. 🔍 ## Print the Default `id` Values @@ -79,7 +79,7 @@ We can confirm that by printing our heroes before adding them to the database: {!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:23-31]!} -# Code below ommitted 👇 +# Code below omitted 👇 ```
@@ -98,7 +98,7 @@ That will output: ```console $ python app.py -// Output above ommitted 👆 +// Output above omitted 👆 Before interacting with the database Hero 1: id=None name='Deadpond' secret_name='Dive Wilson' age=None @@ -118,7 +118,7 @@ What happens when we `add` these objects to the **session**? After we add the `Hero` instance objects to the **session**, the IDs are *still* `None`. -We can verify by creating a session using a `with` block, and adding the objects. And then printing them again: +We can verify by creating a session using a `with` block and adding the objects. And then printing them again: ```Python hl_lines="19-21" # Code above omitted 👆 @@ -144,7 +144,7 @@ This will, again, output the `id`s of the objects as `None`: ```console $ python app.py -// Output above ommitted 👆 +// Output above omitted 👆 After adding to the session Hero 1: id=None name='Deadpond' secret_name='Dive Wilson' age=None @@ -165,7 +165,7 @@ Then we can `commit` the changes in the session, and print again: {!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:33-48]!} -# Code below ommitted 👇 +# Code below omitted 👇 ```
@@ -184,7 +184,7 @@ And now, something unexpected happens, look at the output, it seems as if the `H ```console $ python app.py -// Output above ommitted 👆 +// Output above omitted 👆 // Here the engine talks to the database, the SQL part INFO Engine BEGIN (implicit) diff --git a/docs/tutorial/fastapi/delete.md b/docs/tutorial/fastapi/delete.md index 2ce3fe5b8b..a48122304b 100644 --- a/docs/tutorial/fastapi/delete.md +++ b/docs/tutorial/fastapi/delete.md @@ -39,6 +39,6 @@ After deleting it successfully, we just return a response of: ## Recap -That's it, feel free to try it out in the interactve docs UI to delete some heroes. 💥 +That's it, feel free to try it out in the interactive docs UI to delete some heroes. 💥 Using **FastAPI** to read data and combining it with **SQLModel** makes it quite straightforward to delete data from the database. diff --git a/docs/tutorial/fastapi/limit-and-offset.md b/docs/tutorial/fastapi/limit-and-offset.md index 57043ceaf7..92bbfc7ee0 100644 --- a/docs/tutorial/fastapi/limit-and-offset.md +++ b/docs/tutorial/fastapi/limit-and-offset.md @@ -2,14 +2,14 @@ When a client sends a request to get all the heroes, we have been returning them all. -But if we had **thousands** of heroes that could consume a lot of **computational resources**, network bandwith, etc. +But if we had **thousands** of heroes that could consume a lot of **computational resources**, network bandwidth, etc. -So we probably want to limit it. +So, we probably want to limit it. Let's use the same **offset** and **limit** we learned about in the previous tutorial chapters for the API. !!! info - In many cases this is also called **pagination**. + In many cases, this is also called **pagination**. ## Add a Limit and Offset to the Query Parameters @@ -38,13 +38,13 @@ And by default, we will return a maximum of `100` heroes, so `limit` will have a
-We want to allow clients to set a different `offset` and `limit` values. +We want to allow clients to set different `offset` and `limit` values. But we don't want them to be able to set a `limit` of something like `9999`, that's over `9000`! 😱 So, to prevent it, we add additional validation to the `limit` query parameter, declaring that it has to be **l**ess **t**han or **e**qual to `100` with `lte=100`. -This way, a client can decide to take less heroes if they want, but not more. +This way, a client can decide to take fewer heroes if they want, but not more. !!! info If you need to refresh how query parameters and their validation work, check out the docs in FastAPI: diff --git a/docs/tutorial/fastapi/multiple-models.md b/docs/tutorial/fastapi/multiple-models.md index 3643ec8fcc..c37fad386b 100644 --- a/docs/tutorial/fastapi/multiple-models.md +++ b/docs/tutorial/fastapi/multiple-models.md @@ -2,7 +2,7 @@ We have been using the same `Hero` model to declare the schema of the data we receive in the API, the table model in the database, and the schema of the data we send back in responses. -But in most of the cases there are slight differences, let's use multiple models to solve it. +But in most of the cases, there are slight differences. Let's use multiple models to solve it. Here you will see the main and biggest feature of **SQLModel**. 😎 @@ -10,7 +10,7 @@ Here you will see the main and biggest feature of **SQLModel**. 😎 Let's start by reviewing the automatically generated schemas from the docs UI. -For input we have: +For input, we have: Interactive API docs UI @@ -20,7 +20,7 @@ This means that the client could try to use the same ID that already exists in t That's not what we want. -We want the client to only send the data that is needed to create a new hero: +We want the client only to send the data that is needed to create a new hero: * `name` * `secret_name` @@ -63,7 +63,7 @@ The ultimate goal of an API is for some **clients to use it**. The clients could be a frontend application, a command line program, a graphical user interface, a mobile application, another backend application, etc. -And the code those clients write depend on what our API tells them they **need to send**, and what they can **expect to receive**. +And the code those clients write depends on what our API tells them they **need to send**, and what they can **expect to receive**. Making both sides very clear will make it much easier to interact with the API. @@ -164,7 +164,7 @@ Let's first check how is the process to create a hero now: Let's check that in detail. -Now we use the type annotation `HeroCreate` for the request JSON data, in the `hero` parameter of the **path operation function**. +Now we use the type annotation `HeroCreate` for the request JSON data in the `hero` parameter of the **path operation function**. ```Python hl_lines="3" # Code above omitted 👆 @@ -180,9 +180,9 @@ The method `.from_orm()` reads data from another object with attributes and crea The alternative is `Hero.parse_obj()` that reads data from a dictionary. -But as in this case we have a `HeroCreate` instance in the `hero` variable, this is an object with attributes, so we use `.from_orm()` to read those attributes. +But as in this case, we have a `HeroCreate` instance in the `hero` variable. This is an object with attributes, so we use `.from_orm()` to read those attributes. -With this we create a new `Hero` instance (the one for the database) and put it in the variable `db_hero` from the data in the `hero` variable that is the `HeroCreate` instance we received from the request. +With this, we create a new `Hero` instance (the one for the database) and put it in the variable `db_hero` from the data in the `hero` variable that is the `HeroCreate` instance we received from the request. ```Python hl_lines="3" # Code above omitted 👆 @@ -192,7 +192,7 @@ With this we create a new `Hero` instance (the one for the database) and put it # Code below omitted 👇 ``` -Then we just `add` it to the **session**, `commit`, and `refresh` it, and finally we return the same `db_hero` variable that has the just refreshed `Hero` instance. +Then we just `add` it to the **session**, `commit`, and `refresh` it, and finally, we return the same `db_hero` variable that has the just refreshed `Hero` instance. Because it is just refreshed, it has the `id` field set with a new ID taken from the database. @@ -206,30 +206,30 @@ And now that we return it, FastAPI will validate the data with the `response_mod # Code below omitted 👇 ``` -This will validate that all the data that we promised is there, and will remove any data we didn't declare. +This will validate that all the data that we promised is there and will remove any data we didn't declare. !!! tip - This filtering could be very important, and could be a very good security feature, for example to make sure you filter private data, hashed passwords, etc. + This filtering could be very important and could be a very good security feature, for example, to make sure you filter private data, hashed passwords, etc. You can read more about it in the FastAPI docs about Response Model. -In particular, it will make sure that the `id` is there, and that it is indeed an integer (and not `None`). +In particular, it will make sure that the `id` is there and that it is indeed an integer (and not `None`). ## Shared Fields But looking closely, we could see that these models have a lot of **duplicated information**. -All **the 3 models** declare that thay share some **common fields** that look exactly the same: +All **the 3 models** declare that they share some **common fields** that look exactly the same: * `name`, required * `secret_name`, required * `age`, optional -And then they declare other fields with some differences (in this case only about the `id`). +And then they declare other fields with some differences (in this case, only about the `id`). We want to **avoid duplicated information** if possible. -This is important if, for example, in the future we decide to **refactor the code** and rename one field (column). For example, from `secret_name` to `secret_identity`. +This is important if, for example, in the future, we decide to **refactor the code** and rename one field (column). For example, from `secret_name` to `secret_identity`. If we have that duplicated in multiple models, we could easily forget to update one of them. But if we **avoid duplication**, there's only one place that would need updating. ✨ @@ -363,7 +363,7 @@ This means that there's nothing else special in this class apart from the fact t As an alternative, we could use `HeroBase` directly in the API code instead of `HeroCreate`, but it would show up in the automatic docs UI with that name "`HeroBase`" which could be **confusing** for clients. Instead, "`HeroCreate`" is a bit more explicit about what it is for. -On top of that, we could easily decide in the future that we want to receive **more data** when creating a new hero apart from the data in `HeroBase` (for example a password), and now we already have the class to put those extra fields. +On top of that, we could easily decide in the future that we want to receive **more data** when creating a new hero apart from the data in `HeroBase` (for example, a password), and now we already have the class to put those extra fields. ### The `HeroRead` **Data Model** @@ -390,7 +390,7 @@ This one just declares that the `id` field is required when reading a hero from ## Review the Updated Docs UI -The FastAPI code is still the same as above, we still use `Hero`, `HeroCreate`, and `HeroRead`. But now we define them in a smarter way with inheritance. +The FastAPI code is still the same as above, we still use `Hero`, `HeroCreate`, and `HeroRead`. But now, we define them in a smarter way with inheritance. So, we can jump to the docs UI right away and see how they look with the updated data. @@ -400,7 +400,7 @@ Let's see the new UI for creating a hero: Interactive API docs UI -Nice! It now shows that to create a hero, we just pass the `name`, `secret_name`, and optinally `age`. +Nice! It now shows that to create a hero, we just pass the `name`, `secret_name`, and optionally `age`. We no longer pass an `id`. @@ -416,7 +416,7 @@ And if we check the schema for the **Read Heroes** *path operation* it will also ## Inheritance and Table Models -We just saw how powerful inheritance of these models can be. +We just saw how powerful the inheritance of these models could be. This is a very simple example, and it might look a bit... meh. 😅 diff --git a/docs/tutorial/fastapi/read-one.md b/docs/tutorial/fastapi/read-one.md index b503546298..8eea6488b1 100644 --- a/docs/tutorial/fastapi/read-one.md +++ b/docs/tutorial/fastapi/read-one.md @@ -42,7 +42,7 @@ But if the integer is not the ID of any hero in the database, it will not find a So, we check it in an `if` block, if it's `None`, we raise an `HTTPException` with a `404` status code. -And to use it we first import `HTTPException` from `fastapi`. +And to use it, we first import `HTTPException` from `fastapi`. This will let the client know that they probably made a mistake on their side and requested a hero that doesn't exist in the database. diff --git a/docs/tutorial/fastapi/relationships.md b/docs/tutorial/fastapi/relationships.md index 3aa8863f2f..78ef330fc1 100644 --- a/docs/tutorial/fastapi/relationships.md +++ b/docs/tutorial/fastapi/relationships.md @@ -102,7 +102,7 @@ In this case, we used `response_model=TeamRead` and `response_model=HeroRead`, s Now let's stop for a second and think about it. -We cannot simply include *all* the data including all the internal relationships, because each **hero** has an attribute `team` with their team, and then that **team** also has an attribute `heroes` with all the **heroes** in the team, including this one. +We cannot simply include *all* the data, including all the internal relationships, because each **hero** has an attribute `team` with their team, and then that **team** also has an attribute `heroes` with all the **heroes** in the team, including this one. If we tried to include everything, we could make the server application **crash** trying to extract **infinite data**, going through the same hero and team over and over again internally, something like this: @@ -152,7 +152,7 @@ If we tried to include everything, we could make the server application **crash* } ``` -As you can see, in this example we would get the hero **Rusty-Man**, and from this hero we would get the team **Preventers**, and then from this team we would get its heroes, of course, including **Rusty-Man**... 😱 +As you can see, in this example, we would get the hero **Rusty-Man**, and from this hero we would get the team **Preventers**, and then from this team we would get its heroes, of course, including **Rusty-Man**... 😱 So we start again, and in the end, the server would just crash trying to get all the data with a `"Maximum recursion error"`, we would not even get a response like the one above. @@ -164,7 +164,7 @@ This is a decision that will depend on **each application**. In our case, let's say that if we get a **list of heroes**, we don't want to also include each of their teams in each one. -And if we get a **list of teams**, we don't want to get a a list of the heroes for each one. +And if we get a **list of teams**, we don't want to get a list of the heroes for each one. But if we get a **single hero**, we want to include the team data (without the team's heroes). @@ -195,7 +195,7 @@ We'll add them **after** the other models so that we can easily reference the pr
-These two models are very **simple in code**, but there's a lot happening here, let's check it out. +These two models are very **simple in code**, but there's a lot happening here. Let's check it out. ### Inheritance and Type Annotations @@ -203,7 +203,7 @@ The `HeroReadWithTeam` **inherits** from `HeroRead`, which means that it will ha And then it adds the **new field** `team`, which could be `None`, and is declared with the type `TeamRead` with the base fields for reading a team. -Then we do the same for the `TeamReadWithHeroes`, it **inherits** from `TeamRead`, and declare the **new field** `heroes` which is a list of `HeroRead`. +Then we do the same for the `TeamReadWithHeroes`, it **inherits** from `TeamRead`, and declares the **new field** `heroes`, which is a list of `HeroRead`. ### Data Models Without Relationship Attributes @@ -213,7 +213,7 @@ Instead, here these are only **data models** that will tell FastAPI **which attr ### Reference to Other Models -Also notice that the field `team` is not declared with this new `TeamReadWithHeroes`, because that would again create that infinite recursion of data. Instead, we declare it with the normal `TeamRead` model. +Also, notice that the field `team` is not declared with this new `TeamReadWithHeroes`, because that would again create that infinite recursion of data. Instead, we declare it with the normal `TeamRead` model. And the same for `TeamReadWithHeroes`, the model used for the new field `heroes` uses `HeroRead` to get only each hero's data. @@ -326,7 +326,7 @@ Now we get the list of **heroes** included: ## Recap -Using the same techniques to declare additonal **data models** we can tell FastAPI what data to return in the responses, even when we return **table models**. +Using the same techniques to declare additional **data models**, we can tell FastAPI what data to return in the responses, even when we return **table models**. Here we almost **didn't have to change the FastAPI app** code, but of course, there will be cases where you need to get the data and process it in different ways in the *path operation function* before returning it. @@ -334,4 +334,4 @@ But even in those cases, you will be able to define the **data models** to use i By this point, you already have a very robust API to handle data in a SQL database combining **SQLModel** with **FastAPI**, and implementing **best practices**, like data validation, conversion, filtering, and documentation. ✨ -In the next chapter I'll tell you how to implement automated **testing** for your application using FastAPI and SQLModel. ✅ +In the next chapter, I'll tell you how to implement automated **testing** for your application using FastAPI and SQLModel. ✅ diff --git a/docs/tutorial/fastapi/response-model.md b/docs/tutorial/fastapi/response-model.md index b4e0b6701e..c019f4580b 100644 --- a/docs/tutorial/fastapi/response-model.md +++ b/docs/tutorial/fastapi/response-model.md @@ -22,7 +22,7 @@ You can see that there's a possible "Successful Response" with a code `200`, but API docs UI without response data schemas -Right now we only tell FastAPI the data we want to receive, but we don't tell it yet the data we want to send back. +Right now, we only tell FastAPI the data we want to receive, but we don't tell it yet the data we want to send back. Let's do that now. 🤓 diff --git a/docs/tutorial/fastapi/session-with-dependency.md b/docs/tutorial/fastapi/session-with-dependency.md index 7f049f5002..52a800b9ea 100644 --- a/docs/tutorial/fastapi/session-with-dependency.md +++ b/docs/tutorial/fastapi/session-with-dependency.md @@ -90,7 +90,7 @@ We import `Depends()` from `fastapi`. Then we use it in the *path operation func You can read more about it in the FastAPI documentation Path Parameters and Numeric Validations - Order the parameters as you need, tricks -The value of a dependency will **only be used for one request**, FastAPI will call it right before calling your code, and will give you the value from that dependency. +The value of a dependency will **only be used for one request**, FastAPI will call it right before calling your code and will give you the value from that dependency. If it had `yield`, then it will continue the rest of the execution once you are done sending the response. In the case of the **session**, it will finish the cleanup code from the `with` block, closing the session, etc. diff --git a/docs/tutorial/fastapi/simple-hero-api.md b/docs/tutorial/fastapi/simple-hero-api.md index 5730186726..53a5fa7d38 100644 --- a/docs/tutorial/fastapi/simple-hero-api.md +++ b/docs/tutorial/fastapi/simple-hero-api.md @@ -158,7 +158,7 @@ Here we use the **same** class model to define the **request body** that will be Because **FastAPI** is based on Pydantic, it will use the same model (the Pydantic part) to do automatic data validation and conversion from the JSON request to an object that is an actual instance of the `Hero` class. -And then because this same **SQLModel** object is not only a **Pydantic** model instance but also a **SQLAlchemy** model instance, we can use it directly in a **session** to create the row in the database. +And then, because this same **SQLModel** object is not only a **Pydantic** model instance but also a **SQLAlchemy** model instance, we can use it directly in a **session** to create the row in the database. So we can use intuitive standard Python **type annotations**, and we don't have to duplicate a lot of the code for the database models and the API data models. 🎉 @@ -190,13 +190,13 @@ When a client sends a request to the **path** `/heroes/` with a `GET` HTTP **ope ## One Session per Request -Remember that we shoud use a SQLModel **session** per each group of operations and if we need other unrelated operations we should use a different session? +Remember that we should use a SQLModel **session** per each group of operations and if we need other unrelated operations we should use a different session? Here it is much more obvious. We should normally have **one session per request** in most of the cases. -In some isolated cases we would want to have new sessions inside, so, **more than one session** per request. +In some isolated cases, we would want to have new sessions inside, so, **more than one session** per request. But we would **never want to *share* the same session** among different requests. @@ -277,7 +277,7 @@ And then you can get them back with the **Read Heroes** *path operation*: Now you can terminate that Uvicorn server by going back to the terminal and pressing Ctrl+C. -And then you can open **DB Browser for SQLite** and check the database, to explore the data and confirm that it indeed saved the heroes. 🎉 +And then, you can open **DB Browser for SQLite** and check the database, to explore the data and confirm that it indeed saved the heroes. 🎉 DB Browser for SQLite showing the heroes @@ -287,4 +287,4 @@ Good job! This is already a FastAPI **web API** application to interact with the There are several things we can improve and extend. For example, we want the database to decide the ID of each new hero, we don't want to allow a user to send it. -We will do all those improvements in the next chapters. 🚀 +We will make all those improvements in the next chapters. 🚀 diff --git a/docs/tutorial/fastapi/teams.md b/docs/tutorial/fastapi/teams.md index 9bc4af78cf..7a307b87f5 100644 --- a/docs/tutorial/fastapi/teams.md +++ b/docs/tutorial/fastapi/teams.md @@ -12,7 +12,7 @@ Let's add the models for the teams. It's the same process we did for heroes, with a base model, a **table model**, and some other **data models**. -We have a `TeamBase` **data model**, and from it we inherit with a `Team` **table model**. +We have a `TeamBase` **data model**, and from it, we inherit with a `Team` **table model**. Then we also inherit from the `TeamBase` for the `TeamCreate` and `TeamRead` **data models**. @@ -108,9 +108,9 @@ These are equivalent and very similar to the **path operations** for the **heroe ## Using Relationships Attributes -Up to this point we are actually not using the **relationship attributes**, but we could access them in our code. +Up to this point, we are actually not using the **relationship attributes**, but we could access them in our code. -In the next chapter we will play more with them. +In the next chapter, we will play more with them. ## Check the Docs UI diff --git a/docs/tutorial/fastapi/tests.md b/docs/tutorial/fastapi/tests.md index db71121f94..f817a883a1 100644 --- a/docs/tutorial/fastapi/tests.md +++ b/docs/tutorial/fastapi/tests.md @@ -76,7 +76,7 @@ Let's start with a simple test, with just the basic test code we need the check That's the **core** of the code we need for all the tests later. -But now we need to deal with a bit of logistics and details we are not paying attention to just yet. 🤓 +But now, we need to deal with a bit of logistics and details we are not paying attention to just yet. 🤓 ## Testing Database @@ -155,7 +155,7 @@ That way, when we call `.create_all()` all the **table models** are correctly re ## Memory Database -Now we are not using the production database, instead we use a **new testing database** with the `testing.db` file, which is great. +Now we are not using the production database. Instead, we use a **new testing database** with the `testing.db` file, which is great. But SQLite also supports having an **in memory** database. This means that all the database is only in memory, and it is never saved in a file on disk. @@ -171,7 +171,7 @@ Other alternatives and ideas 👀 Before arriving at the idea of using an **in-memory database** we could have explored other alternatives and ideas. -The first, is that we are not deleting the file after we finish the test, so, the next test could have **leftover data**. So, the right thing would be to delete the file right after finishing the test. 🔥 +The first is that we are not deleting the file after we finish the test, so the next test could have **leftover data**. So, the right thing would be to delete the file right after finishing the test. 🔥 But if each test has to create a new file and then delete it afterwards, running all the tests could be **a bit slow**. @@ -179,7 +179,7 @@ Right now, we have a file `testing.db` that is used by all the tests (we only ha So, if we tried to run the tests at the same time **in parallel** to try to speed things up a bit, they would clash trying to use the *same* `testing.db` file. -Of couse, we could also fix that, using some **random name** for each testing database file... but in the case of SQLite, we have an even better alternative with just using an **in-memory database**. ✨ +Of course, we could also fix that, using some **random name** for each testing database file... but in the case of SQLite, we have an even better alternative by just using an **in-memory database**. ✨
@@ -208,7 +208,7 @@ And all the other tests can do the same. Great, that works, and you could replicate all that process in each of the test functions. -But we had to add a lot of **boilerplate code** to handle the custom database, creating it in memory, the custom session, the dependency override. +But we had to add a lot of **boilerplate code** to handle the custom database, creating it in memory, the custom session, and the dependency override. Do we really have to duplicate all that for **each test**? No, we can do better! 😎 @@ -242,12 +242,12 @@ Let's see the first code example with a fixture: **pytest** fixtures work in a very similar way to FastAPI dependencies, but have some minor differences: -* In pytest fixtures we need to add a decorator of `@pytest.fixture()` on top. +* In pytest fixtures, we need to add a decorator of `@pytest.fixture()` on top. * To use a pytest fixture in a function, we have to declare the parameter with the **exact same name**. In FastAPI we have to **explicitly use `Depends()`** with the actual function inside it. But apart from the way we declare them and how we tell the framework that we want to have them in the function, they **work in a very similar way**. -Now we create lot's of tests, and re-use that same fixture in all of them, saving us that **boilerplate code**. +Now we create lot's of tests and re-use that same fixture in all of them, saving us that **boilerplate code**. **pytest** will make sure to run them right before (and finish them right after) each test function. So, each test function will actually have its own database, engine, and session. @@ -255,7 +255,7 @@ Now we create lot's of tests, and re-use that same fixture in all of them, savin Awesome, that fixture helps us prevent a lot of duplicated code. -But currently we still have to write some code in the test function that will be repetitive for other tests, right now we: +But currently, we still have to write some code in the test function that will be repetitive for other tests, right now we: * create the **dependency override** * put it in the `app.dependency_overrides` @@ -277,7 +277,7 @@ So, we can create a **client fixture** that will be used in all the tests, and i !!! tip Check out the number bubbles to see what is done by each line of code. -Now we have a **client fixture** that in turns uses the **session fixture**. +Now we have a **client fixture** that, in turn, uses the **session fixture**. And in the actual test function, we just have to declare that we require this **client fixture**. @@ -285,7 +285,7 @@ And in the actual test function, we just have to declare that we require this ** At this point, it all might seem like we just did a lot of changes for nothing, to get **the same result**. 🤔 -But normally we will create **lots of other test functions**. And now all the boilerplate and complexity is **writen only once**, in those two fixtures. +But normally we will create **lots of other test functions**. And now all the boilerplate and complexity is **written only once**, in those two fixtures. Let's add some more tests: @@ -315,9 +315,9 @@ Now, any additional test functions can be as **simple** as the first one, they j ## Why Two Fixtures -Now, seeing the code we could think, why do we put **two fixtures** instead of **just one** with all the code? And that makes total sense! +Now, seeing the code, we could think, why do we put **two fixtures** instead of **just one** with all the code? And that makes total sense! -For these examples, **that would have been simpler**, there's no need to separate that code in two fixtures for them... +For these examples, **that would have been simpler**, there's no need to separate that code into two fixtures for them... But for the next test function, we will require **both fixtures**, the **client** and the **session**. @@ -340,7 +340,7 @@ But for the next test function, we will require **both fixtures**, the **client* -In this test function we want to check that the *path operation* to **read a list of heroes** actually sends us heroes. +In this test function, we want to check that the *path operation* to **read a list of heroes** actually sends us heroes. But if the **database is empty**, we would get an **empty list**, and we wouldn't know if the hero data is being sent correctly or not. @@ -362,7 +362,7 @@ The function for the **client fixture** and the actual testing function will **b ## Add the Rest of the Tests -Using the same ideas, requiring the fixtures, creating data that we need for the tests, etc. we can now add the rest of the tests, they look quite similar to what we have done up to now. +Using the same ideas, requiring the fixtures, creating data that we need for the tests, etc., we can now add the rest of the tests. They look quite similar to what we have done up to now. ```Python hl_lines="3 18 33" # Code above omitted 👆 @@ -406,9 +406,9 @@ project/test_main.py ....... [100%] Did you read all that? Wow, I'm impressed! 😎 -Adding tests to your application will give you a lot of **certainty** that everything is **working correctly**, as you indended. +Adding tests to your application will give you a lot of **certainty** that everything is **working correctly**, as you intended. -And tests will be notoriously useful when **refactoring** your code, **changing things**, **adding features**. Because tests they can help catch a lot of errors that can be easily introduced by refactoring. +And tests will be notoriously useful when **refactoring** your code, **changing things**, **adding features**. Because tests can help catch a lot of errors that can be easily introduced by refactoring. And they will give you the confidence to work faster and **more efficiently**, because you know that you are checking if you are **not breaking anything**. 😅 diff --git a/docs/tutorial/fastapi/update.md b/docs/tutorial/fastapi/update.md index b845d5a22c..0b5292bd29 100644 --- a/docs/tutorial/fastapi/update.md +++ b/docs/tutorial/fastapi/update.md @@ -61,7 +61,7 @@ We will use a `PATCH` HTTP operation. This is used to **partially update data**, -We also read the `hero_id` from the *path parameter* an the request body, a `HeroUpdate`. +We also read the `hero_id` from the *path parameter* and the request body, a `HeroUpdate`. ### Read the Existing Hero @@ -100,7 +100,7 @@ But that also means that if we just call `hero.dict()` we will get a dictionary } ``` -And then if we update the hero in the database with this data, we would be removing any existing values, and that's probably **not what the client intended**. +And then, if we update the hero in the database with this data, we would be removing any existing values, and that's probably **not what the client intended**. But fortunately Pydantic models (and so SQLModel models) have a parameter we can pass to the `.dict()` method for that: `exclude_unset=True`. @@ -200,7 +200,7 @@ We are **not simply omitting** the data that has the **default values**. And we are **not simply omitting** anything that is `None`. -This means that, if a model in the database **has a value different than the default**, the client could **reset it to the same value as the default**, or even `None`, and we would **still notice it** and **update it accordingly**. 🤯🚀 +This means that if a model in the database **has a value different than the default**, the client could **reset it to the same value as the default**, or even `None`, and we would **still notice it** and **update it accordingly**. 🤯🚀 So, if the client wanted to intentionally remove the `age` of a hero, they could just send a JSON with: @@ -222,7 +222,7 @@ So, we would use that value and update the `age` to `None` in the database, **ju Notice that `age` here is `None`, and **we still detected it**. -Also that `name` was not even sent, and we don't *accidentally* set it to `None` or something, we just didn't touch it, because the client didn't sent it, so we are **perfectly fine**, even in these corner cases. ✨ +Also, that `name` was not even sent, and we don't *accidentally* set it to `None` or something. We just didn't touch it because the client didn't send it, so we are **perfectly fine**, even in these corner cases. ✨ These are some of the advantages of Pydantic, that we can use with SQLModel. 🎉 From 14fc1f510e0b03185cc6dc94cf8ce164c7a811cc Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 21:23:41 +0000 Subject: [PATCH 102/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index db9d65520f..0ecfee3af0 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typos in multiple files in the docs. PR [#400](https://github.com/tiangolo/sqlmodel/pull/400) by [@VictorGambarini](https://github.com/VictorGambarini). * ✏ Fix typo in `docs/tutorial/code-structure.md`. PR [#344](https://github.com/tiangolo/sqlmodel/pull/344) by [@marciomazza](https://github.com/marciomazza). * ✏ Fix typo in `docs/db-to-code.md`. PR [#155](https://github.com/tiangolo/sqlmodel/pull/155) by [@gr8jam](https://github.com/gr8jam). * ✏ Fix typo in `docs/contributing.md`. PR [#323](https://github.com/tiangolo/sqlmodel/pull/323) by [@Fardad13](https://github.com/Fardad13). From 87a02b4c466b01b7d8ce7e14fefe1868965e2c89 Mon Sep 17 00:00:00 2001 From: Joe Mudryk Date: Sat, 27 Aug 2022 14:25:29 -0700 Subject: [PATCH 103/906] =?UTF-8?q?=E2=9C=8F=20Fix=20typo=20in=20`docs/tut?= =?UTF-8?q?orial/fastapi/simple-hero-api.md`=20(#80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez From c0a6b2dd8b939439a28319b8b6b0e3975bc77c27 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 21:26:06 +0000 Subject: [PATCH 104/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 0ecfee3af0..8f52729a57 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typo in `docs/tutorial/fastapi/simple-hero-api.md`. PR [#80](https://github.com/tiangolo/sqlmodel/pull/80) by [@joemudryk](https://github.com/joemudryk). * ✏ Fix typos in multiple files in the docs. PR [#400](https://github.com/tiangolo/sqlmodel/pull/400) by [@VictorGambarini](https://github.com/VictorGambarini). * ✏ Fix typo in `docs/tutorial/code-structure.md`. PR [#344](https://github.com/tiangolo/sqlmodel/pull/344) by [@marciomazza](https://github.com/marciomazza). * ✏ Fix typo in `docs/db-to-code.md`. PR [#155](https://github.com/tiangolo/sqlmodel/pull/155) by [@gr8jam](https://github.com/gr8jam). From 0aaf39d539da3636180c99efc120a7ea0e68c651 Mon Sep 17 00:00:00 2001 From: Jorge Alvarado Date: Sat, 27 Aug 2022 17:31:38 -0400 Subject: [PATCH 105/906] =?UTF-8?q?=E2=9C=8F=20Fix=20typo=20in=20`docs/tut?= =?UTF-8?q?orial/relationship-attributes/define-relationships-attributes.m?= =?UTF-8?q?d`=20(#239)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- .../relationship-attributes/define-relationships-attributes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/relationship-attributes/define-relationships-attributes.md b/docs/tutorial/relationship-attributes/define-relationships-attributes.md index 09d7b2765b..0531ec53e5 100644 --- a/docs/tutorial/relationship-attributes/define-relationships-attributes.md +++ b/docs/tutorial/relationship-attributes/define-relationships-attributes.md @@ -96,7 +96,7 @@ Next, use that `Relationship` to declare a new attribute in the model classes: ## What Are These Relationship Attributes -This new attributes are not the same as fields, they **don't represent a column** directly in the database, and their value is not a singular value like an integer. Their value is the actual **entire object** that is related. +These new attributes are not the same as fields, they **don't represent a column** directly in the database, and their value is not a singular value like an integer. Their value is the actual **entire object** that is related. So, in the case of a `Hero` instance, if you call `hero.team`, you will get the entire `Team` instance object that this hero belongs to. ✨ From 5dfef7ede7903259781e0b350bb4e81e5762b56b Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 21:32:10 +0000 Subject: [PATCH 106/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 8f52729a57..673ac46f57 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typo in `docs/tutorial/relationship-attributes/define-relationships-attributes.md`. PR [#239](https://github.com/tiangolo/sqlmodel/pull/239) by [@jalvaradosegura](https://github.com/jalvaradosegura). * ✏ Fix typo in `docs/tutorial/fastapi/simple-hero-api.md`. PR [#80](https://github.com/tiangolo/sqlmodel/pull/80) by [@joemudryk](https://github.com/joemudryk). * ✏ Fix typos in multiple files in the docs. PR [#400](https://github.com/tiangolo/sqlmodel/pull/400) by [@VictorGambarini](https://github.com/VictorGambarini). * ✏ Fix typo in `docs/tutorial/code-structure.md`. PR [#344](https://github.com/tiangolo/sqlmodel/pull/344) by [@marciomazza](https://github.com/marciomazza). From 91d0785b1cb575a62b33f1a5e69f4fec979ef971 Mon Sep 17 00:00:00 2001 From: Prashanth Rao <35005448+prrao87@users.noreply.github.com> Date: Sat, 27 Aug 2022 17:36:58 -0400 Subject: [PATCH 107/906] =?UTF-8?q?=E2=9C=8F=20Fix=20typos=20in=20`docs/da?= =?UTF-8?q?tabases.md`=20and=20`docs/tutorial/index.md`=20(#35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/databases.md | 2 +- docs/tutorial/index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/databases.md b/docs/databases.md index e29c73e506..f1aaf663ab 100644 --- a/docs/databases.md +++ b/docs/databases.md @@ -274,7 +274,7 @@ The language is called **SQL**, the name comes from for **Structured Query Langu Nevertheless, the language is not only used to *query* for data. It is also used to create records/rows, to update them, to delete them. And to manipulate the database, create tables, etc. -This language is supported by all these databases that handle multiple tables, that's why they are called **SQL Databases**. Although, each database has small variations in the SQL language they support. +This language is supported by all these databases that handle multiple tables, that's why they are called **SQL Databases**. Although, each database has small variations in the SQL language they support (*dialect*). Let's imagine that the table holding the heroes is called the `hero` table. An example of a SQL query to get all the data from it could look like: diff --git a/docs/tutorial/index.md b/docs/tutorial/index.md index 398cabafb4..33cf6226c4 100644 --- a/docs/tutorial/index.md +++ b/docs/tutorial/index.md @@ -6,7 +6,7 @@ If you need a refresher about how to use Python type hints (type annotations), c You can also check the mypy cheat sheet. -**SQLModel** uses type annotations for everything, this way you can use a familiar Python syntax and get all the editor support posible, with autocompletion and in-editor error checking. +**SQLModel** uses type annotations for everything, this way you can use a familiar Python syntax and get all the editor support possible, with autocompletion and in-editor error checking. ## Intro From 7d3bf70a7622fd5e6259a523e14594b65bc76f88 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 21:37:31 +0000 Subject: [PATCH 108/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 673ac46f57..89f6e03cfe 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typos in `docs/databases.md` and `docs/tutorial/index.md`. PR [#35](https://github.com/tiangolo/sqlmodel/pull/35) by [@prrao87](https://github.com/prrao87). * ✏ Fix typo in `docs/tutorial/relationship-attributes/define-relationships-attributes.md`. PR [#239](https://github.com/tiangolo/sqlmodel/pull/239) by [@jalvaradosegura](https://github.com/jalvaradosegura). * ✏ Fix typo in `docs/tutorial/fastapi/simple-hero-api.md`. PR [#80](https://github.com/tiangolo/sqlmodel/pull/80) by [@joemudryk](https://github.com/joemudryk). * ✏ Fix typos in multiple files in the docs. PR [#400](https://github.com/tiangolo/sqlmodel/pull/400) by [@VictorGambarini](https://github.com/VictorGambarini). From e48fb2874bc9df048f059a9f819120e36fe3440a Mon Sep 17 00:00:00 2001 From: Jorge Alvarado Date: Sat, 27 Aug 2022 17:55:15 -0400 Subject: [PATCH 109/906] =?UTF-8?q?=F0=9F=8E=A8=20Remove=20unwanted=20high?= =?UTF-8?q?light=20in=20the=20docs=20(#233)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/tutorial/automatic-id-none-refresh.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/automatic-id-none-refresh.md b/docs/tutorial/automatic-id-none-refresh.md index ac6a2a4fca..bbf74dd307 100644 --- a/docs/tutorial/automatic-id-none-refresh.md +++ b/docs/tutorial/automatic-id-none-refresh.md @@ -450,7 +450,7 @@ Now let's review all this code once again. And as we created the **engine** with `echo=True`, we can see the SQL statements being executed at each step. -```{ .python .annotate hl_lines="54" } +```{ .python .annotate } {!./docs_src/tutorial/automatic_id_none_refresh/tutorial002.py!} ``` From ae1b8b558552fbc29286caaede9eee9f314426a8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 21:55:59 +0000 Subject: [PATCH 110/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 89f6e03cfe..673e9000ca 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🎨 Remove unwanted highlight in the docs. PR [#233](https://github.com/tiangolo/sqlmodel/pull/233) by [@jalvaradosegura](https://github.com/jalvaradosegura). * ✏ Fix typos in `docs/databases.md` and `docs/tutorial/index.md`. PR [#35](https://github.com/tiangolo/sqlmodel/pull/35) by [@prrao87](https://github.com/prrao87). * ✏ Fix typo in `docs/tutorial/relationship-attributes/define-relationships-attributes.md`. PR [#239](https://github.com/tiangolo/sqlmodel/pull/239) by [@jalvaradosegura](https://github.com/jalvaradosegura). * ✏ Fix typo in `docs/tutorial/fastapi/simple-hero-api.md`. PR [#80](https://github.com/tiangolo/sqlmodel/pull/80) by [@joemudryk](https://github.com/joemudryk). From ee576ab279b88cedef3e241203b112080c9d7066 Mon Sep 17 00:00:00 2001 From: Yoann Mosteiro <41114561+yoannmos@users.noreply.github.com> Date: Sun, 28 Aug 2022 00:06:56 +0200 Subject: [PATCH 111/906] =?UTF-8?q?=E2=9C=8F=20Fix=20broken=20variable/typ?= =?UTF-8?q?o=20in=20docs=20for=20Read=20Relationships,=20`hero=5Fspider=5F?= =?UTF-8?q?boy.id`=20=3D>=20`hero=5Fspider=5Fboy.team=5Fid`=20(#106)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- .../relationship_attributes/read_relationships/tutorial001.py | 2 +- .../test_read_relationships/test_tutorial001.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py index 5f718cab45..3b130072b7 100644 --- a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py +++ b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py @@ -99,7 +99,7 @@ def select_heroes(): result = session.exec(statement) hero_spider_boy = result.one() - statement = select(Team).where(Team.id == hero_spider_boy.id) + statement = select(Team).where(Team.id == hero_spider_boy.team_id) result = session.exec(statement) team = result.first() print("Spider-Boy's team:", team) diff --git a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001.py b/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001.py index 887c98ee6f..9fc70012d8 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001.py +++ b/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001.py @@ -81,7 +81,7 @@ ], [ "Spider-Boy's team:", - {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, + {"headquarters": "Sharp Tower", "id": 2, "name": "Preventers"}, ], [ "Spider-Boy's team again:", From 2407ecd2bf93968bed17c54ee5954959b90cf02f Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 22:07:38 +0000 Subject: [PATCH 112/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 673e9000ca..3c206f0a9d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix broken variable/typo in docs for Read Relationships, `hero_spider_boy.id` => `hero_spider_boy.team_id`. PR [#106](https://github.com/tiangolo/sqlmodel/pull/106) by [@yoannmos](https://github.com/yoannmos). * 🎨 Remove unwanted highlight in the docs. PR [#233](https://github.com/tiangolo/sqlmodel/pull/233) by [@jalvaradosegura](https://github.com/jalvaradosegura). * ✏ Fix typos in `docs/databases.md` and `docs/tutorial/index.md`. PR [#35](https://github.com/tiangolo/sqlmodel/pull/35) by [@prrao87](https://github.com/prrao87). * ✏ Fix typo in `docs/tutorial/relationship-attributes/define-relationships-attributes.md`. PR [#239](https://github.com/tiangolo/sqlmodel/pull/239) by [@jalvaradosegura](https://github.com/jalvaradosegura). From 9830ee0d8991ffc068ffb72ccead2427c84e58ee Mon Sep 17 00:00:00 2001 From: Evangelos Anagnostopoulos Date: Sun, 28 Aug 2022 01:18:57 +0300 Subject: [PATCH 113/906] =?UTF-8?q?=F0=9F=90=9B=20Fix=20setting=20nullable?= =?UTF-8?q?=20property=20of=20Fields=20that=20don't=20accept=20`None`=20(#?= =?UTF-8?q?79)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- sqlmodel/main.py | 13 ++++++++++++- .../test_create_db_and_table/test_tutorial001.py | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 9efdafeca3..d85976db47 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -25,6 +25,7 @@ from pydantic import BaseConfig, BaseModel from pydantic.errors import ConfigError, DictError +from pydantic.fields import SHAPE_SINGLETON from pydantic.fields import FieldInfo as PydanticFieldInfo from pydantic.fields import ModelField, Undefined, UndefinedType from pydantic.main import ModelMetaclass, validate_model @@ -424,7 +425,6 @@ def get_column_from_field(field: ModelField) -> Column: # type: ignore return sa_column sa_type = get_sqlachemy_type(field) primary_key = getattr(field.field_info, "primary_key", False) - nullable = not field.required index = getattr(field.field_info, "index", Undefined) if index is Undefined: index = False @@ -432,6 +432,7 @@ def get_column_from_field(field: ModelField) -> Column: # type: ignore field_nullable = getattr(field.field_info, "nullable") if field_nullable != Undefined: nullable = field_nullable + nullable = not primary_key and _is_field_nullable(field) args = [] foreign_key = getattr(field.field_info, "foreign_key", None) if foreign_key: @@ -646,3 +647,13 @@ def _calculate_keys( @declared_attr # type: ignore def __tablename__(cls) -> str: return cls.__name__.lower() + + +def _is_field_nullable(field: ModelField) -> bool: + if not field.required: + # Taken from [Pydantic](https://github.com/samuelcolvin/pydantic/blob/v1.8.2/pydantic/fields.py#L946-L947) + is_optional = field.allow_none and ( + field.shape != SHAPE_SINGLETON or not field.sub_fields + ) + return is_optional and field.default is None and field.default_factory is None + return False diff --git a/tests/test_tutorial/test_create_db_and_table/test_tutorial001.py b/tests/test_tutorial/test_create_db_and_table/test_tutorial001.py index 591a51cc22..b6a2e72628 100644 --- a/tests/test_tutorial/test_create_db_and_table/test_tutorial001.py +++ b/tests/test_tutorial/test_create_db_and_table/test_tutorial001.py @@ -9,7 +9,7 @@ def test_create_db_and_table(cov_tmp_path: Path): assert "BEGIN" in result.stdout assert 'PRAGMA main.table_info("hero")' in result.stdout assert "CREATE TABLE hero (" in result.stdout - assert "id INTEGER," in result.stdout + assert "id INTEGER NOT NULL," in result.stdout assert "name VARCHAR NOT NULL," in result.stdout assert "secret_name VARCHAR NOT NULL," in result.stdout assert "age INTEGER," in result.stdout From db3ad598c5a0f3baab0fc4fbe8acbe6eb7580878 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 22:19:35 +0000 Subject: [PATCH 114/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 3c206f0a9d..4c42f9536f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix setting nullable property of Fields that don't accept `None`. PR [#79](https://github.com/tiangolo/sqlmodel/pull/79) by [@van51](https://github.com/van51). * ✏ Fix broken variable/typo in docs for Read Relationships, `hero_spider_boy.id` => `hero_spider_boy.team_id`. PR [#106](https://github.com/tiangolo/sqlmodel/pull/106) by [@yoannmos](https://github.com/yoannmos). * 🎨 Remove unwanted highlight in the docs. PR [#233](https://github.com/tiangolo/sqlmodel/pull/233) by [@jalvaradosegura](https://github.com/jalvaradosegura). * ✏ Fix typos in `docs/databases.md` and `docs/tutorial/index.md`. PR [#35](https://github.com/tiangolo/sqlmodel/pull/35) by [@prrao87](https://github.com/prrao87). From 5ea9340def1f2580c919c8be10024aeaeb5d038c Mon Sep 17 00:00:00 2001 From: Andrew Bolster Date: Sat, 27 Aug 2022 23:28:09 +0100 Subject: [PATCH 115/906] =?UTF-8?q?=E2=9C=A8=20Update=20GUID=20handling=20?= =?UTF-8?q?to=20use=20stdlib=20`UUID.hex`=20instead=20of=20an=20`int`=20(#?= =?UTF-8?q?26)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- sqlmodel/sql/sqltypes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sqlmodel/sql/sqltypes.py b/sqlmodel/sql/sqltypes.py index b3fda87739..a9f53ad286 100644 --- a/sqlmodel/sql/sqltypes.py +++ b/sqlmodel/sql/sqltypes.py @@ -47,10 +47,10 @@ def process_bind_param(self, value: Any, dialect: Dialect) -> Optional[str]: return str(value) else: if not isinstance(value, uuid.UUID): - return f"{uuid.UUID(value).int:x}" + return uuid.UUID(value).hex else: # hexstring - return f"{value.int:x}" + return value.hex def process_result_value(self, value: Any, dialect: Dialect) -> Optional[uuid.UUID]: if value is None: From 2fab4817fe1c265cc7eabd4fef57de9253aaa063 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 22:28:46 +0000 Subject: [PATCH 116/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 4c42f9536f..bff7d8bc51 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Update GUID handling to use stdlib `UUID.hex` instead of an `int`. PR [#26](https://github.com/tiangolo/sqlmodel/pull/26) by [@andrewbolster](https://github.com/andrewbolster). * 🐛 Fix setting nullable property of Fields that don't accept `None`. PR [#79](https://github.com/tiangolo/sqlmodel/pull/79) by [@van51](https://github.com/van51). * ✏ Fix broken variable/typo in docs for Read Relationships, `hero_spider_boy.id` => `hero_spider_boy.team_id`. PR [#106](https://github.com/tiangolo/sqlmodel/pull/106) by [@yoannmos](https://github.com/yoannmos). * 🎨 Remove unwanted highlight in the docs. PR [#233](https://github.com/tiangolo/sqlmodel/pull/233) by [@jalvaradosegura](https://github.com/jalvaradosegura). From eef0b7770b03be0a5e016c444cc1362c46f688c0 Mon Sep 17 00:00:00 2001 From: Chris White Date: Sat, 27 Aug 2022 15:48:44 -0700 Subject: [PATCH 117/906] =?UTF-8?q?=F0=9F=90=9B=20Fix=20Enum=20handling=20?= =?UTF-8?q?in=20SQLAlchemy=20(#165)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- sqlmodel/main.py | 17 +++-------- tests/test_enums.py | 72 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 13 deletions(-) create mode 100644 tests/test_enums.py diff --git a/sqlmodel/main.py b/sqlmodel/main.py index d85976db47..86e28b3333 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -31,18 +31,9 @@ from pydantic.main import ModelMetaclass, validate_model from pydantic.typing import ForwardRef, NoArgAnyCallable, resolve_annotations from pydantic.utils import ROOT_KEY, Representation -from sqlalchemy import ( - Boolean, - Column, - Date, - DateTime, - Float, - ForeignKey, - Integer, - Interval, - Numeric, - inspect, -) +from sqlalchemy import Boolean, Column, Date, DateTime +from sqlalchemy import Enum as sa_Enum +from sqlalchemy import Float, ForeignKey, Integer, Interval, Numeric, inspect from sqlalchemy.orm import RelationshipProperty, declared_attr, registry, relationship from sqlalchemy.orm.attributes import set_attribute from sqlalchemy.orm.decl_api import DeclarativeMeta @@ -396,7 +387,7 @@ def get_sqlachemy_type(field: ModelField) -> Any: if issubclass(field.type_, time): return Time if issubclass(field.type_, Enum): - return Enum + return sa_Enum(field.type_) if issubclass(field.type_, bytes): return LargeBinary if issubclass(field.type_, Decimal): diff --git a/tests/test_enums.py b/tests/test_enums.py new file mode 100644 index 0000000000..aeec6456da --- /dev/null +++ b/tests/test_enums.py @@ -0,0 +1,72 @@ +import enum +import uuid + +from sqlalchemy import create_mock_engine +from sqlalchemy.sql.type_api import TypeEngine +from sqlmodel import Field, SQLModel + +""" +Tests related to Enums + +Associated issues: +* https://github.com/tiangolo/sqlmodel/issues/96 +* https://github.com/tiangolo/sqlmodel/issues/164 +""" + + +class MyEnum1(enum.Enum): + A = "A" + B = "B" + + +class MyEnum2(enum.Enum): + C = "C" + D = "D" + + +class BaseModel(SQLModel): + id: uuid.UUID = Field(primary_key=True) + enum_field: MyEnum2 + + +class FlatModel(SQLModel, table=True): + id: uuid.UUID = Field(primary_key=True) + enum_field: MyEnum1 + + +class InheritModel(BaseModel, table=True): + pass + + +def pg_dump(sql: TypeEngine, *args, **kwargs): + dialect = sql.compile(dialect=postgres_engine.dialect) + sql_str = str(dialect).rstrip() + if sql_str: + print(sql_str + ";") + + +def sqlite_dump(sql: TypeEngine, *args, **kwargs): + dialect = sql.compile(dialect=sqlite_engine.dialect) + sql_str = str(dialect).rstrip() + if sql_str: + print(sql_str + ";") + + +postgres_engine = create_mock_engine("postgresql://", pg_dump) +sqlite_engine = create_mock_engine("sqlite://", sqlite_dump) + + +def test_postgres_ddl_sql(capsys): + SQLModel.metadata.create_all(bind=postgres_engine, checkfirst=False) + + captured = capsys.readouterr() + assert "CREATE TYPE myenum1 AS ENUM ('A', 'B');" in captured.out + assert "CREATE TYPE myenum2 AS ENUM ('C', 'D');" in captured.out + + +def test_sqlite_ddl_sql(capsys): + SQLModel.metadata.create_all(bind=sqlite_engine, checkfirst=False) + + captured = capsys.readouterr() + assert "enum_field VARCHAR(1) NOT NULL" in captured.out + assert "CREATE TYPE" not in captured.out From 9c68ce12ec5d3fcf129dbcdc6566d9187ab81c0e Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 22:49:17 +0000 Subject: [PATCH 118/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index bff7d8bc51..19327dc574 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix Enum handling in SQLAlchemy. PR [#165](https://github.com/tiangolo/sqlmodel/pull/165) by [@chriswhite199](https://github.com/chriswhite199). * ✨ Update GUID handling to use stdlib `UUID.hex` instead of an `int`. PR [#26](https://github.com/tiangolo/sqlmodel/pull/26) by [@andrewbolster](https://github.com/andrewbolster). * 🐛 Fix setting nullable property of Fields that don't accept `None`. PR [#79](https://github.com/tiangolo/sqlmodel/pull/79) by [@van51](https://github.com/van51). * ✏ Fix broken variable/typo in docs for Read Relationships, `hero_spider_boy.id` => `hero_spider_boy.team_id`. PR [#106](https://github.com/tiangolo/sqlmodel/pull/106) by [@yoannmos](https://github.com/yoannmos). From 680602b7eb6723b1ea0f217e9122cd66d2866360 Mon Sep 17 00:00:00 2001 From: statt8900 <30441880+statt8900@users.noreply.github.com> Date: Sat, 27 Aug 2022 18:59:09 -0400 Subject: [PATCH 119/906] =?UTF-8?q?=F0=9F=90=9B=20Fix=20fields=20marked=20?= =?UTF-8?q?as=20"set"=20in=20models=20(#117)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michael Statt Co-authored-by: Sebastián Ramírez --- sqlmodel/main.py | 2 +- tests/test_fields_set.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 tests/test_fields_set.py diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 86e28b3333..8af077dafe 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -499,9 +499,9 @@ def __init__(__pydantic_self__, **data: Any) -> None: # Do not set values as in Pydantic, pass them through setattr, so SQLAlchemy # can handle them # object.__setattr__(__pydantic_self__, '__dict__', values) - object.__setattr__(__pydantic_self__, "__fields_set__", fields_set) for key, value in values.items(): setattr(__pydantic_self__, key, value) + object.__setattr__(__pydantic_self__, "__fields_set__", fields_set) non_pydantic_keys = data.keys() - values.keys() for key in non_pydantic_keys: if key in __pydantic_self__.__sqlmodel_relationships__: diff --git a/tests/test_fields_set.py b/tests/test_fields_set.py new file mode 100644 index 0000000000..56f4ad0144 --- /dev/null +++ b/tests/test_fields_set.py @@ -0,0 +1,21 @@ +from datetime import datetime, timedelta + +from sqlmodel import Field, SQLModel + + +def test_fields_set(): + class User(SQLModel): + username: str + email: str = "test@test.com" + last_updated: datetime = Field(default_factory=datetime.now) + + user = User(username="bob") + assert user.__fields_set__ == {"username"} + user = User(username="bob", email="bob@test.com") + assert user.__fields_set__ == {"username", "email"} + user = User( + username="bob", + email="bob@test.com", + last_updated=datetime.now() - timedelta(days=1), + ) + assert user.__fields_set__ == {"username", "email", "last_updated"} From 71d6fcc31bad4c95095f08e4f83eba4715375f92 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 22:59:54 +0000 Subject: [PATCH 120/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 19327dc574..f38aa2b996 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix fields marked as "set" in models. PR [#117](https://github.com/tiangolo/sqlmodel/pull/117) by [@statt8900](https://github.com/statt8900). * 🐛 Fix Enum handling in SQLAlchemy. PR [#165](https://github.com/tiangolo/sqlmodel/pull/165) by [@chriswhite199](https://github.com/chriswhite199). * ✨ Update GUID handling to use stdlib `UUID.hex` instead of an `int`. PR [#26](https://github.com/tiangolo/sqlmodel/pull/26) by [@andrewbolster](https://github.com/andrewbolster). * 🐛 Fix setting nullable property of Fields that don't accept `None`. PR [#79](https://github.com/tiangolo/sqlmodel/pull/79) by [@van51](https://github.com/van51). From d38073604342ef334160ca4b1ff5616ba2a7302c Mon Sep 17 00:00:00 2001 From: byrman Date: Sun, 28 Aug 2022 01:10:23 +0200 Subject: [PATCH 121/906] =?UTF-8?q?=F0=9F=90=9B=20Fix=20handling=20validat?= =?UTF-8?q?ors=20for=20non-default=20values=20(#253)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- sqlmodel/main.py | 2 +- tests/test_validation.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 tests/test_validation.py diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 8af077dafe..0144f6f4aa 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -582,7 +582,7 @@ def validate(cls: Type["SQLModel"], value: Any) -> "SQLModel": values, fields_set, validation_error = validate_model(cls, value) if validation_error: raise validation_error - model = cls(**values) + model = cls(**value) # Reset fields set, this would have been done in Pydantic in __init__ object.__setattr__(model, "__fields_set__", fields_set) return model diff --git a/tests/test_validation.py b/tests/test_validation.py new file mode 100644 index 0000000000..a3ff6e39ba --- /dev/null +++ b/tests/test_validation.py @@ -0,0 +1,33 @@ +from typing import Optional + +import pytest +from pydantic import validator +from pydantic.error_wrappers import ValidationError +from sqlmodel import SQLModel + + +def test_validation(clear_sqlmodel): + """Test validation of implicit and explict None values. + + # For consistency with pydantic, validators are not to be called on + # arguments that are not explicitly provided. + + https://github.com/tiangolo/sqlmodel/issues/230 + https://github.com/samuelcolvin/pydantic/issues/1223 + + """ + + class Hero(SQLModel): + name: Optional[str] = None + secret_name: Optional[str] = None + age: Optional[int] = None + + @validator("name", "secret_name", "age") + def reject_none(cls, v): + assert v is not None + return v + + Hero.validate({"age": 25}) + + with pytest.raises(ValidationError): + Hero.validate({"name": None, "age": 25}) From 5e0ac5b56ccc72d7f643463ae1cb2af302863e3a Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 23:11:00 +0000 Subject: [PATCH 122/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index f38aa2b996..bb9d275506 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix handling validators for non-default values. PR [#253](https://github.com/tiangolo/sqlmodel/pull/253) by [@byrman](https://github.com/byrman). * 🐛 Fix fields marked as "set" in models. PR [#117](https://github.com/tiangolo/sqlmodel/pull/117) by [@statt8900](https://github.com/statt8900). * 🐛 Fix Enum handling in SQLAlchemy. PR [#165](https://github.com/tiangolo/sqlmodel/pull/165) by [@chriswhite199](https://github.com/chriswhite199). * ✨ Update GUID handling to use stdlib `UUID.hex` instead of an `int`. PR [#26](https://github.com/tiangolo/sqlmodel/pull/26) by [@andrewbolster](https://github.com/andrewbolster). From 475578757f7746fd5a0e4d45a32df44cce474874 Mon Sep 17 00:00:00 2001 From: Rabin Adhikari Date: Sun, 28 Aug 2022 05:02:37 +0545 Subject: [PATCH 123/906] =?UTF-8?q?=F0=9F=90=9B=20Fix=20`Select`=20and=20`?= =?UTF-8?q?SelectOfScalar`=20to=20inherit=20cache=20to=20avoid=20warning:?= =?UTF-8?q?=20`SAWarning:=20Class=20SelectOfScalar=20will=20not=20make=20u?= =?UTF-8?q?se=20of=20SQL=20compilation=20caching`=20(#234)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- sqlmodel/sql/expression.py.jinja2 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sqlmodel/sql/expression.py.jinja2 b/sqlmodel/sql/expression.py.jinja2 index 033130393a..51f04a215d 100644 --- a/sqlmodel/sql/expression.py.jinja2 +++ b/sqlmodel/sql/expression.py.jinja2 @@ -27,14 +27,14 @@ _TSelect = TypeVar("_TSelect") if sys.version_info.minor >= 7: class Select(_Select, Generic[_TSelect]): - pass + inherit_cache = True # This is not comparable to sqlalchemy.sql.selectable.ScalarSelect, that has a different # purpose. This is the same as a normal SQLAlchemy Select class where there's only one # entity, so the result will be converted to a scalar by default. This way writing # for loops on the results will feel natural. class SelectOfScalar(_Select, Generic[_TSelect]): - pass + inherit_cache = True else: from typing import GenericMeta # type: ignore @@ -43,10 +43,10 @@ else: pass class _Py36Select(_Select, Generic[_TSelect], metaclass=GenericSelectMeta): - pass + inherit_cache = True class _Py36SelectOfScalar(_Select, Generic[_TSelect], metaclass=GenericSelectMeta): - pass + inherit_cache = True # Cast them for editors to work correctly, from several tricks tried, this works # for both VS Code and PyCharm From c743647a52390d1f5767b2bcaf540ae13d0eb530 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 23:18:19 +0000 Subject: [PATCH 124/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index bb9d275506..66bd840620 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix `Select` and `SelectOfScalar` to inherit cache to avoid warning: `SAWarning: Class SelectOfScalar will not make use of SQL compilation caching`. PR [#234](https://github.com/tiangolo/sqlmodel/pull/234) by [@rabinadk1](https://github.com/rabinadk1). * 🐛 Fix handling validators for non-default values. PR [#253](https://github.com/tiangolo/sqlmodel/pull/253) by [@byrman](https://github.com/byrman). * 🐛 Fix fields marked as "set" in models. PR [#117](https://github.com/tiangolo/sqlmodel/pull/117) by [@statt8900](https://github.com/statt8900). * 🐛 Fix Enum handling in SQLAlchemy. PR [#165](https://github.com/tiangolo/sqlmodel/pull/165) by [@chriswhite199](https://github.com/chriswhite199). From 5429e9b6aaeaed40bbcabe5acf0b7bde28721e88 Mon Sep 17 00:00:00 2001 From: phi-friday Date: Sun, 28 Aug 2022 08:22:09 +0900 Subject: [PATCH 125/906] =?UTF-8?q?=F0=9F=90=9B=20Fix=20type=20annotations?= =?UTF-8?q?=20for=20`Model.parse=5Fobj()`,=20and=20`Model.validate()`=20(#?= =?UTF-8?q?321)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- sqlmodel/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 0144f6f4aa..bdfe6dfc1c 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -558,8 +558,8 @@ def from_orm( @classmethod def parse_obj( - cls: Type["SQLModel"], obj: Any, update: Optional[Dict[str, Any]] = None - ) -> "SQLModel": + cls: Type[_TSQLModel], obj: Any, update: Optional[Dict[str, Any]] = None + ) -> _TSQLModel: obj = cls._enforce_dict_if_root(obj) # SQLModel, support update dict if update is not None: @@ -573,7 +573,7 @@ def __repr_args__(self) -> Sequence[Tuple[Optional[str], Any]]: # From Pydantic, override to enforce validation with dict @classmethod - def validate(cls: Type["SQLModel"], value: Any) -> "SQLModel": + def validate(cls: Type[_TSQLModel], value: Any) -> _TSQLModel: if isinstance(value, cls): return value.copy() if cls.__config__.copy_on_model_validation else value From 8ac82e7101f0e3fcc8c3f568ac03ad0e8e638b38 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 23:22:53 +0000 Subject: [PATCH 126/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 66bd840620..63fea941ef 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix type annotations for `Model.parse_obj()`, and `Model.validate()`. PR [#321](https://github.com/tiangolo/sqlmodel/pull/321) by [@phi-friday](https://github.com/phi-friday). * 🐛 Fix `Select` and `SelectOfScalar` to inherit cache to avoid warning: `SAWarning: Class SelectOfScalar will not make use of SQL compilation caching`. PR [#234](https://github.com/tiangolo/sqlmodel/pull/234) by [@rabinadk1](https://github.com/rabinadk1). * 🐛 Fix handling validators for non-default values. PR [#253](https://github.com/tiangolo/sqlmodel/pull/253) by [@byrman](https://github.com/byrman). * 🐛 Fix fields marked as "set" in models. PR [#117](https://github.com/tiangolo/sqlmodel/pull/117) by [@statt8900](https://github.com/statt8900). From a2cda8377fa4c9e5a75d751571d715032bd5033e Mon Sep 17 00:00:00 2001 From: kurtportelli <47539697+kurtportelli@users.noreply.github.com> Date: Sun, 28 Aug 2022 01:43:42 +0200 Subject: [PATCH 127/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20docs=20for=20mo?= =?UTF-8?q?dels=20for=20updating,=20`id`=20should=20not=20be=20updatable?= =?UTF-8?q?=20(#335)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/tutorial/fastapi/relationships.md | 8 ++++---- docs/tutorial/fastapi/teams.md | 12 ++++++------ docs_src/tutorial/fastapi/teams/tutorial001.py | 1 - .../test_fastapi/test_teams/test_tutorial001.py | 1 - 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/docs/tutorial/fastapi/relationships.md b/docs/tutorial/fastapi/relationships.md index 78ef330fc1..6921b5ac85 100644 --- a/docs/tutorial/fastapi/relationships.md +++ b/docs/tutorial/fastapi/relationships.md @@ -55,11 +55,11 @@ And the same way, we declared the `TeamRead` with only the same base fields of t # Code here omitted 👈 -{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:32-37]!} +{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:31-36]!} # Code here omitted 👈 -{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:46-47]!} +{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:45-46]!} # Code below omitted 👇 ``` @@ -80,11 +80,11 @@ In this case, we used `response_model=TeamRead` and `response_model=HeroRead`, s ```Python hl_lines="3 8 12 17" # Code above omitted 👆 -{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:105-110]!} +{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:104-109]!} # Code here omitted 👈 -{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:160-165]!} +{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:159-164]!} # Code below omitted 👇 ``` diff --git a/docs/tutorial/fastapi/teams.md b/docs/tutorial/fastapi/teams.md index 7a307b87f5..0b19a95cb3 100644 --- a/docs/tutorial/fastapi/teams.md +++ b/docs/tutorial/fastapi/teams.md @@ -18,8 +18,8 @@ Then we also inherit from the `TeamBase` for the `TeamCreate` and `TeamRead` **d And we also create a `TeamUpdate` **data model**. -```Python hl_lines="7-9 12-15 18-19 22-23 26-29" -{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:1-29]!} +```Python hl_lines="7-9 12-15 18-19 22-23 26-28" +{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:1-28]!} # Code below omitted 👇 ``` @@ -42,7 +42,7 @@ Let's now update the `Hero` models too. ```Python hl_lines="3-8 11-15 17-18 21-22 25-29" # Code above omitted 👆 -{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:32-58]!} +{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:31-57]!} # Code below omitted 👇 ``` @@ -66,10 +66,10 @@ And even though the `HeroBase` is *not* a **table model**, we can declare `team_ Notice that the **relationship attributes**, the ones with `Relationship()`, are **only** in the **table models**, as those are the ones that are handled by **SQLModel** with SQLAlchemy and that can have the automatic fetching of data from the database when we access them. -```Python hl_lines="11 39" +```Python hl_lines="11 38" # Code above omitted 👆 -{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:7-58]!} +{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:7-57]!} # Code below omitted 👇 ``` @@ -92,7 +92,7 @@ These are equivalent and very similar to the **path operations** for the **heroe ```Python hl_lines="3-9 12-20 23-28 31-47 50-57" # Code above omitted 👆 -{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:140-194]!} +{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:139-193]!} # Code below omitted 👇 ``` diff --git a/docs_src/tutorial/fastapi/teams/tutorial001.py b/docs_src/tutorial/fastapi/teams/tutorial001.py index e8f88b8e9e..2a0bd600fc 100644 --- a/docs_src/tutorial/fastapi/teams/tutorial001.py +++ b/docs_src/tutorial/fastapi/teams/tutorial001.py @@ -24,7 +24,6 @@ class TeamRead(TeamBase): class TeamUpdate(SQLModel): - id: Optional[int] = None name: Optional[str] = None headquarters: Optional[str] = None diff --git a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py index 852839b2a1..6ac1cffc5e 100644 --- a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py @@ -442,7 +442,6 @@ "title": "TeamUpdate", "type": "object", "properties": { - "id": {"title": "Id", "type": "integer"}, "name": {"title": "Name", "type": "string"}, "headquarters": {"title": "Headquarters", "type": "string"}, }, From 1ca288028cb7da598d162bcf914d5183cd8f2aed Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 23:44:16 +0000 Subject: [PATCH 128/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 63fea941ef..5589bb7495 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Update docs for models for updating, `id` should not be updatable. PR [#335](https://github.com/tiangolo/sqlmodel/pull/335) by [@kurtportelli](https://github.com/kurtportelli). * 🐛 Fix type annotations for `Model.parse_obj()`, and `Model.validate()`. PR [#321](https://github.com/tiangolo/sqlmodel/pull/321) by [@phi-friday](https://github.com/phi-friday). * 🐛 Fix `Select` and `SelectOfScalar` to inherit cache to avoid warning: `SAWarning: Class SelectOfScalar will not make use of SQL compilation caching`. PR [#234](https://github.com/tiangolo/sqlmodel/pull/234) by [@rabinadk1](https://github.com/rabinadk1). * 🐛 Fix handling validators for non-default values. PR [#253](https://github.com/tiangolo/sqlmodel/pull/253) by [@byrman](https://github.com/byrman). From 42b0e6eace676a0e7ee5eb42bf20206b5fc90219 Mon Sep 17 00:00:00 2001 From: Raphael Gibson <42935757+raphaelgibson@users.noreply.github.com> Date: Sat, 27 Aug 2022 20:49:29 -0300 Subject: [PATCH 129/906] =?UTF-8?q?=E2=9C=A8=20Allow=20setting=20`unique`?= =?UTF-8?q?=20in=20`Field()`=20for=20a=20column=20(#83)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Raphael Gibson Co-authored-by: Sebastián Ramírez --- sqlmodel/main.py | 6 +++ tests/test_main.py | 93 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 tests/test_main.py diff --git a/sqlmodel/main.py b/sqlmodel/main.py index bdfe6dfc1c..7c79edd2e3 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -61,6 +61,7 @@ def __init__(self, default: Any = Undefined, **kwargs: Any) -> None: primary_key = kwargs.pop("primary_key", False) nullable = kwargs.pop("nullable", Undefined) foreign_key = kwargs.pop("foreign_key", Undefined) + unique = kwargs.pop("unique", False) index = kwargs.pop("index", Undefined) sa_column = kwargs.pop("sa_column", Undefined) sa_column_args = kwargs.pop("sa_column_args", Undefined) @@ -80,6 +81,7 @@ def __init__(self, default: Any = Undefined, **kwargs: Any) -> None: self.primary_key = primary_key self.nullable = nullable self.foreign_key = foreign_key + self.unique = unique self.index = index self.sa_column = sa_column self.sa_column_args = sa_column_args @@ -141,6 +143,7 @@ def Field( regex: Optional[str] = None, primary_key: bool = False, foreign_key: Optional[Any] = None, + unique: bool = False, nullable: Union[bool, UndefinedType] = Undefined, index: Union[bool, UndefinedType] = Undefined, sa_column: Union[Column, UndefinedType] = Undefined, # type: ignore @@ -171,6 +174,7 @@ def Field( regex=regex, primary_key=primary_key, foreign_key=foreign_key, + unique=unique, nullable=nullable, index=index, sa_column=sa_column, @@ -426,12 +430,14 @@ def get_column_from_field(field: ModelField) -> Column: # type: ignore nullable = not primary_key and _is_field_nullable(field) args = [] foreign_key = getattr(field.field_info, "foreign_key", None) + unique = getattr(field.field_info, "unique", False) if foreign_key: args.append(ForeignKey(foreign_key)) kwargs = { "primary_key": primary_key, "nullable": nullable, "index": index, + "unique": unique, } sa_default = Undefined if field.field_info.default_factory: diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000000..22c62327da --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,93 @@ +from typing import Optional + +import pytest +from sqlalchemy.exc import IntegrityError +from sqlmodel import Field, Session, SQLModel, create_engine + + +def test_should_allow_duplicate_row_if_unique_constraint_is_not_passed(clear_sqlmodel): + class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str + secret_name: str + age: Optional[int] = None + + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Deadpond", secret_name="Dive Wilson") + + engine = create_engine("sqlite://") + + SQLModel.metadata.create_all(engine) + + with Session(engine) as session: + session.add(hero_1) + session.commit() + session.refresh(hero_1) + + with Session(engine) as session: + session.add(hero_2) + session.commit() + session.refresh(hero_2) + + with Session(engine) as session: + heroes = session.query(Hero).all() + assert len(heroes) == 2 + assert heroes[0].name == heroes[1].name + + +def test_should_allow_duplicate_row_if_unique_constraint_is_false(clear_sqlmodel): + class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str + secret_name: str = Field(unique=False) + age: Optional[int] = None + + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Deadpond", secret_name="Dive Wilson") + + engine = create_engine("sqlite://") + + SQLModel.metadata.create_all(engine) + + with Session(engine) as session: + session.add(hero_1) + session.commit() + session.refresh(hero_1) + + with Session(engine) as session: + session.add(hero_2) + session.commit() + session.refresh(hero_2) + + with Session(engine) as session: + heroes = session.query(Hero).all() + assert len(heroes) == 2 + assert heroes[0].name == heroes[1].name + + +def test_should_raise_exception_when_try_to_duplicate_row_if_unique_constraint_is_true( + clear_sqlmodel, +): + class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str + secret_name: str = Field(unique=True) + age: Optional[int] = None + + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Deadpond", secret_name="Dive Wilson") + + engine = create_engine("sqlite://") + + SQLModel.metadata.create_all(engine) + + with Session(engine) as session: + session.add(hero_1) + session.commit() + session.refresh(hero_1) + + with pytest.raises(IntegrityError): + with Session(engine) as session: + session.add(hero_2) + session.commit() + session.refresh(hero_2) From 2bc915ed04e7a28fc5b567f46748705874e5c6f6 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 23:50:09 +0000 Subject: [PATCH 130/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 5589bb7495..2622a58203 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Allow setting `unique` in `Field()` for a column. PR [#83](https://github.com/tiangolo/sqlmodel/pull/83) by [@raphaelgibson](https://github.com/raphaelgibson). * 📝 Update docs for models for updating, `id` should not be updatable. PR [#335](https://github.com/tiangolo/sqlmodel/pull/335) by [@kurtportelli](https://github.com/kurtportelli). * 🐛 Fix type annotations for `Model.parse_obj()`, and `Model.validate()`. PR [#321](https://github.com/tiangolo/sqlmodel/pull/321) by [@phi-friday](https://github.com/phi-friday). * 🐛 Fix `Select` and `SelectOfScalar` to inherit cache to avoid warning: `SAWarning: Class SelectOfScalar will not make use of SQL compilation caching`. PR [#234](https://github.com/tiangolo/sqlmodel/pull/234) by [@rabinadk1](https://github.com/rabinadk1). From 92f52a3fc56a3a504b9f19e7bf3d2f3a773a58b1 Mon Sep 17 00:00:00 2001 From: Amin Alaee Date: Sun, 28 Aug 2022 01:50:12 +0200 Subject: [PATCH 131/906] =?UTF-8?q?=E2=99=BB=20Refactor=20internal=20impor?= =?UTF-8?q?ts=20to=20reduce=20redundancy=20(#272)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- sqlmodel/sql/sqltypes.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sqlmodel/sql/sqltypes.py b/sqlmodel/sql/sqltypes.py index a9f53ad286..09b8239476 100644 --- a/sqlmodel/sql/sqltypes.py +++ b/sqlmodel/sql/sqltypes.py @@ -1,11 +1,10 @@ import uuid from typing import Any, Optional, cast -from sqlalchemy import types +from sqlalchemy import CHAR, types from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.engine.interfaces import Dialect from sqlalchemy.sql.type_api import TypeEngine -from sqlalchemy.types import CHAR, TypeDecorator class AutoString(types.TypeDecorator): # type: ignore @@ -23,7 +22,7 @@ def load_dialect_impl(self, dialect: Dialect) -> "types.TypeEngine[Any]": # Reference form SQLAlchemy docs: https://docs.sqlalchemy.org/en/14/core/custom_types.html#backend-agnostic-guid-type # with small modifications -class GUID(TypeDecorator): # type: ignore +class GUID(types.TypeDecorator): # type: ignore """Platform-independent GUID type. Uses PostgreSQL's UUID type, otherwise uses From 6fe256ec2c84a68733653efbd85e6964f14845d9 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 23:50:48 +0000 Subject: [PATCH 132/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 2622a58203..c62b3af1f3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ♻ Refactor internal imports to reduce redundancy. PR [#272](https://github.com/tiangolo/sqlmodel/pull/272) by [@aminalaee](https://github.com/aminalaee). * ✨ Allow setting `unique` in `Field()` for a column. PR [#83](https://github.com/tiangolo/sqlmodel/pull/83) by [@raphaelgibson](https://github.com/raphaelgibson). * 📝 Update docs for models for updating, `id` should not be updatable. PR [#335](https://github.com/tiangolo/sqlmodel/pull/335) by [@kurtportelli](https://github.com/kurtportelli). * 🐛 Fix type annotations for `Model.parse_obj()`, and `Model.validate()`. PR [#321](https://github.com/tiangolo/sqlmodel/pull/321) by [@phi-friday](https://github.com/phi-friday). From 6216409f9630433a765095ab0bbeb9762edbc70e Mon Sep 17 00:00:00 2001 From: Yasser Tahiri Date: Sun, 28 Aug 2022 00:53:02 +0100 Subject: [PATCH 133/906] =?UTF-8?q?=E2=99=BB=20Refactor=20internal=20state?= =?UTF-8?q?ments=20to=20simplify=20code=20(#53)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/main.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 7c79edd2e3..a5ce8faf74 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -519,9 +519,8 @@ def __setattr__(self, name: str, value: Any) -> None: return else: # Set in SQLAlchemy, before Pydantic to trigger events and updates - if getattr(self.__config__, "table", False): - if is_instrumented(self, name): - set_attribute(self, name, value) + if getattr(self.__config__, "table", False) and is_instrumented(self, name): + set_attribute(self, name, value) # Set in Pydantic model to trigger possible validation changes, only for # non relationship values if name not in self.__sqlmodel_relationships__: @@ -611,7 +610,7 @@ def _calculate_keys( exclude_unset: bool, update: Optional[Dict[str, Any]] = None, ) -> Optional[AbstractSet[str]]: - if include is None and exclude is None and exclude_unset is False: + if include is None and exclude is None and not exclude_unset: # Original in Pydantic: # return None # Updated to not return SQLAlchemy attributes @@ -629,7 +628,6 @@ def _calculate_keys( # Do not include relationships as that would easily lead to infinite # recursion, or traversing the whole database keys = self.__fields__.keys() # | self.__sqlmodel_relationships__.keys() - if include is not None: keys &= include.keys() From eb12bbc640b7b009769b6f3787d1c8de3d80fd57 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 27 Aug 2022 23:53:33 +0000 Subject: [PATCH 134/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index c62b3af1f3..4f343c9648 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ♻ Refactor internal statements to simplify code. PR [#53](https://github.com/tiangolo/sqlmodel/pull/53) by [@yezz123](https://github.com/yezz123). * ♻ Refactor internal imports to reduce redundancy. PR [#272](https://github.com/tiangolo/sqlmodel/pull/272) by [@aminalaee](https://github.com/aminalaee). * ✨ Allow setting `unique` in `Field()` for a column. PR [#83](https://github.com/tiangolo/sqlmodel/pull/83) by [@raphaelgibson](https://github.com/raphaelgibson). * 📝 Update docs for models for updating, `id` should not be updatable. PR [#335](https://github.com/tiangolo/sqlmodel/pull/335) by [@kurtportelli](https://github.com/kurtportelli). From e7848923ecece0b376c0c56e60cba3956702cc6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 28 Aug 2022 01:59:04 +0200 Subject: [PATCH 135/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 4f343c9648..b62c5312dc 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,17 +2,27 @@ ## Latest Changes -* ♻ Refactor internal statements to simplify code. PR [#53](https://github.com/tiangolo/sqlmodel/pull/53) by [@yezz123](https://github.com/yezz123). -* ♻ Refactor internal imports to reduce redundancy. PR [#272](https://github.com/tiangolo/sqlmodel/pull/272) by [@aminalaee](https://github.com/aminalaee). +### Features + * ✨ Allow setting `unique` in `Field()` for a column. PR [#83](https://github.com/tiangolo/sqlmodel/pull/83) by [@raphaelgibson](https://github.com/raphaelgibson). -* 📝 Update docs for models for updating, `id` should not be updatable. PR [#335](https://github.com/tiangolo/sqlmodel/pull/335) by [@kurtportelli](https://github.com/kurtportelli). +* ✨ Update GUID handling to use stdlib `UUID.hex` instead of an `int`. PR [#26](https://github.com/tiangolo/sqlmodel/pull/26) by [@andrewbolster](https://github.com/andrewbolster). +* ✨ Raise an exception when using a Pydantic field type with no matching SQLAlchemy type. PR [#18](https://github.com/tiangolo/sqlmodel/pull/18) by [@elben10](https://github.com/elben10). +* ⬆ Upgrade constrain for SQLAlchemy = ">=1.4.17,<=1.4.41". PR [#371](https://github.com/tiangolo/sqlmodel/pull/371) by [@RobertRosca](https://github.com/RobertRosca). +* ✨ Add new `Session.get()` parameter `execution_options`. PR [#302](https://github.com/tiangolo/sqlmodel/pull/302) by [@tiangolo](https://github.com/tiangolo). + +### Fixes + * 🐛 Fix type annotations for `Model.parse_obj()`, and `Model.validate()`. PR [#321](https://github.com/tiangolo/sqlmodel/pull/321) by [@phi-friday](https://github.com/phi-friday). * 🐛 Fix `Select` and `SelectOfScalar` to inherit cache to avoid warning: `SAWarning: Class SelectOfScalar will not make use of SQL compilation caching`. PR [#234](https://github.com/tiangolo/sqlmodel/pull/234) by [@rabinadk1](https://github.com/rabinadk1). * 🐛 Fix handling validators for non-default values. PR [#253](https://github.com/tiangolo/sqlmodel/pull/253) by [@byrman](https://github.com/byrman). * 🐛 Fix fields marked as "set" in models. PR [#117](https://github.com/tiangolo/sqlmodel/pull/117) by [@statt8900](https://github.com/statt8900). * 🐛 Fix Enum handling in SQLAlchemy. PR [#165](https://github.com/tiangolo/sqlmodel/pull/165) by [@chriswhite199](https://github.com/chriswhite199). -* ✨ Update GUID handling to use stdlib `UUID.hex` instead of an `int`. PR [#26](https://github.com/tiangolo/sqlmodel/pull/26) by [@andrewbolster](https://github.com/andrewbolster). * 🐛 Fix setting nullable property of Fields that don't accept `None`. PR [#79](https://github.com/tiangolo/sqlmodel/pull/79) by [@van51](https://github.com/van51). +* 🐛 Fix SQLAlchemy version 1.4.36 breaks SQLModel relationships (#315). PR [#322](https://github.com/tiangolo/sqlmodel/pull/322) by [@byrman](https://github.com/byrman). + +### Docs + +* 📝 Update docs for models for updating, `id` should not be updatable. PR [#335](https://github.com/tiangolo/sqlmodel/pull/335) by [@kurtportelli](https://github.com/kurtportelli). * ✏ Fix broken variable/typo in docs for Read Relationships, `hero_spider_boy.id` => `hero_spider_boy.team_id`. PR [#106](https://github.com/tiangolo/sqlmodel/pull/106) by [@yoannmos](https://github.com/yoannmos). * 🎨 Remove unwanted highlight in the docs. PR [#233](https://github.com/tiangolo/sqlmodel/pull/233) by [@jalvaradosegura](https://github.com/jalvaradosegura). * ✏ Fix typos in `docs/databases.md` and `docs/tutorial/index.md`. PR [#35](https://github.com/tiangolo/sqlmodel/pull/35) by [@prrao87](https://github.com/prrao87). @@ -36,19 +46,20 @@ * ✏ Fix typo in `docs/tutorial/where.md`. PR [#72](https://github.com/tiangolo/sqlmodel/pull/72) by [@ZettZet](https://github.com/ZettZet). * ✏ Fix typo in `docs/tutorial/code-structure.md`. PR [#91](https://github.com/tiangolo/sqlmodel/pull/91) by [@dhiraj](https://github.com/dhiraj). * ✏ Fix broken link to newsletter sign-up in `docs/help.md`. PR [#84](https://github.com/tiangolo/sqlmodel/pull/84) by [@mborus](https://github.com/mborus). -* ⬆ Update development requirement for FastAPI from `^0.68.0` to `^0.68.1`. PR [#48](https://github.com/tiangolo/sqlmodel/pull/48) by [@alucarddelta](https://github.com/alucarddelta). * ✏ Fix typos in `docs/tutorial/many-to-many/create-models-with-link.md`. PR [#45](https://github.com/tiangolo/sqlmodel/pull/45) by [@xginn8](https://github.com/xginn8). -* ✨ Raise an exception when using a Pydantic field type with no matching SQLAlchemy type. PR [#18](https://github.com/tiangolo/sqlmodel/pull/18) by [@elben10](https://github.com/elben10). +* ✏ Fix typo in `docs/tutorial/index.md`. PR [#398](https://github.com/tiangolo/sqlmodel/pull/398) by [@ryangrose](https://github.com/ryangrose). + +### Internal + +* ♻ Refactor internal statements to simplify code. PR [#53](https://github.com/tiangolo/sqlmodel/pull/53) by [@yezz123](https://github.com/yezz123). +* ♻ Refactor internal imports to reduce redundancy. PR [#272](https://github.com/tiangolo/sqlmodel/pull/272) by [@aminalaee](https://github.com/aminalaee). +* ⬆ Update development requirement for FastAPI from `^0.68.0` to `^0.68.1`. PR [#48](https://github.com/tiangolo/sqlmodel/pull/48) by [@alucarddelta](https://github.com/alucarddelta). * ⏪ Revert upgrade Poetry, to make a release that supports Python 3.6 first. PR [#417](https://github.com/tiangolo/sqlmodel/pull/417) by [@tiangolo](https://github.com/tiangolo). * 👷 Add dependabot for GitHub Actions. PR [#410](https://github.com/tiangolo/sqlmodel/pull/410) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade Poetry to version `==1.2.0b1`. PR [#303](https://github.com/tiangolo/sqlmodel/pull/303) by [@tiangolo](https://github.com/tiangolo). -* ✏ Fix typo in `docs/tutorial/index.md`. PR [#398](https://github.com/tiangolo/sqlmodel/pull/398) by [@ryangrose](https://github.com/ryangrose). -* ⬆ Upgrade constrain for SQLAlchemy = ">=1.4.17,<=1.4.41". PR [#371](https://github.com/tiangolo/sqlmodel/pull/371) by [@RobertRosca](https://github.com/RobertRosca). -* 🐛 Fix SQLAlchemy version 1.4.36 breaks SQLModel relationships (#315). PR [#322](https://github.com/tiangolo/sqlmodel/pull/322) by [@byrman](https://github.com/byrman). * 👷 Add CI for Python 3.10. PR [#305](https://github.com/tiangolo/sqlmodel/pull/305) by [@tiangolo](https://github.com/tiangolo). * 📝 Add Jina's QA Bot to the docs to help people that want to ask quick questions. PR [#263](https://github.com/tiangolo/sqlmodel/pull/263) by [@tiangolo](https://github.com/tiangolo). * 👷 Upgrade Codecov GitHub Action. PR [#304](https://github.com/tiangolo/sqlmodel/pull/304) by [@tiangolo](https://github.com/tiangolo). -* ✨ Add new `Session.get()` parameter `execution_options`. PR [#302](https://github.com/tiangolo/sqlmodel/pull/302) by [@tiangolo](https://github.com/tiangolo). * 💚 Only run CI on push when on master, to avoid duplicate runs on PRs. PR [#244](https://github.com/tiangolo/sqlmodel/pull/244) by [@tiangolo](https://github.com/tiangolo). * 🔧 Upgrade MkDocs Material and update configs. PR [#217](https://github.com/tiangolo/sqlmodel/pull/217) by [@tiangolo](https://github.com/tiangolo). * ⬆ Upgrade mypy, fix type annotations. PR [#218](https://github.com/tiangolo/sqlmodel/pull/218) by [@tiangolo](https://github.com/tiangolo). From f9522b391304d1eab329e7fc62606aa03fa98b5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 28 Aug 2022 01:59:44 +0200 Subject: [PATCH 136/906] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.0.?= =?UTF-8?q?7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 3 +++ sqlmodel/__init__.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index b62c5312dc..e0662eabfa 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.0.7 + ### Features * ✨ Allow setting `unique` in `Field()` for a column. PR [#83](https://github.com/tiangolo/sqlmodel/pull/83) by [@raphaelgibson](https://github.com/raphaelgibson). diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index 12eb5d569b..8c57237115 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.0.6" +__version__ = "0.0.7" # Re-export from SQLAlchemy from sqlalchemy.engine import create_mock_engine as create_mock_engine From 4143edd2513b3db9e683bb65c63a23f9f3a7c12c Mon Sep 17 00:00:00 2001 From: Theodore Williams Date: Mon, 29 Aug 2022 02:33:41 -0600 Subject: [PATCH 137/906] =?UTF-8?q?=E2=9C=8F=20Fix=20typo=20in=20`docs/tut?= =?UTF-8?q?orial/connect/remove-data-connections.md`=20(#421)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tutorial/connect/remove-data-connections.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/connect/remove-data-connections.md b/docs/tutorial/connect/remove-data-connections.md index 1153b51f32..f44559b3d1 100644 --- a/docs/tutorial/connect/remove-data-connections.md +++ b/docs/tutorial/connect/remove-data-connections.md @@ -46,7 +46,7 @@ We will continue with the code from the previous chapter. ## Break a Connection -We don't really have to delete anyting to break a connection. We can just assign `None` to the foreign key, in this case, to the `team_id`. +We don't really have to delete anything to break a connection. We can just assign `None` to the foreign key, in this case, to the `team_id`. Let's say **Spider-Boy** is tired of the lack of friendly neighbors and wants to get out of the **Preventers**. From f232166db5e4eb4e21b86e1c6ff40f4de514d8d8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 29 Aug 2022 08:34:17 +0000 Subject: [PATCH 138/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index e0662eabfa..c3eec7a906 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typo in `docs/tutorial/connect/remove-data-connections.md`. PR [#421](https://github.com/tiangolo/sqlmodel/pull/421) by [@VerdantFox](https://github.com/VerdantFox). ## 0.0.7 From b51ebaf658d3835ac476fdb09cb22cbc8e07cfcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 29 Aug 2022 11:44:08 +0200 Subject: [PATCH 139/906] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Update=20`expresio?= =?UTF-8?q?n.py`,=20sync=20from=20Jinja2=20template,=20implement=20`inheri?= =?UTF-8?q?t=5Fcache`=20to=20solve=20errors=20like:=20`SAWarning:=20Class?= =?UTF-8?q?=20SelectOfScalar=20will=20not=20make=20use=20of=20SQL=20compil?= =?UTF-8?q?ation=20caching`=20(#422)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/generate_select.py | 8 ++++++++ scripts/lint.sh | 2 ++ sqlmodel/sql/expression.py | 8 ++++---- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/scripts/generate_select.py b/scripts/generate_select.py index b66a1673c4..f8aa30023f 100644 --- a/scripts/generate_select.py +++ b/scripts/generate_select.py @@ -1,3 +1,4 @@ +import os from itertools import product from pathlib import Path from typing import List, Tuple @@ -52,4 +53,11 @@ class Arg(BaseModel): result = black.format_str(result, mode=black.Mode()) +current_content = destiny_path.read_text() + +if current_content != result and os.getenv("CHECK_JINJA"): + raise RuntimeError( + "sqlmodel/sql/expression.py content not update with Jinja2 template" + ) + destiny_path.write_text(result) diff --git a/scripts/lint.sh b/scripts/lint.sh index 4191d90f1f..02568cda6b 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -7,3 +7,5 @@ mypy sqlmodel flake8 sqlmodel tests docs_src black sqlmodel tests docs_src --check isort sqlmodel tests docs_src scripts --check-only +# TODO: move this to test.sh after deprecating Python 3.6 +CHECK_JINJA=1 python scripts/generate_select.py diff --git a/sqlmodel/sql/expression.py b/sqlmodel/sql/expression.py index e7317bcdd8..31c0bc1a1e 100644 --- a/sqlmodel/sql/expression.py +++ b/sqlmodel/sql/expression.py @@ -29,14 +29,14 @@ if sys.version_info.minor >= 7: class Select(_Select, Generic[_TSelect]): - pass + inherit_cache = True # This is not comparable to sqlalchemy.sql.selectable.ScalarSelect, that has a different # purpose. This is the same as a normal SQLAlchemy Select class where there's only one # entity, so the result will be converted to a scalar by default. This way writing # for loops on the results will feel natural. class SelectOfScalar(_Select, Generic[_TSelect]): - pass + inherit_cache = True else: from typing import GenericMeta # type: ignore @@ -45,10 +45,10 @@ class GenericSelectMeta(GenericMeta, _Select.__class__): # type: ignore pass class _Py36Select(_Select, Generic[_TSelect], metaclass=GenericSelectMeta): - pass + inherit_cache = True class _Py36SelectOfScalar(_Select, Generic[_TSelect], metaclass=GenericSelectMeta): - pass + inherit_cache = True # Cast them for editors to work correctly, from several tricks tried, this works # for both VS Code and PyCharm From 85f5e7fc45c3333ec2174e8010e49cdada982fc2 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 29 Aug 2022 09:44:50 +0000 Subject: [PATCH 140/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index c3eec7a906..6e668f89c6 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ♻️ Update `expresion.py`, sync from Jinja2 template, implement `inherit_cache` to solve errors like: `SAWarning: Class SelectOfScalar will not make use of SQL compilation caching`. PR [#422](https://github.com/tiangolo/sqlmodel/pull/422) by [@tiangolo](https://github.com/tiangolo). * ✏ Fix typo in `docs/tutorial/connect/remove-data-connections.md`. PR [#421](https://github.com/tiangolo/sqlmodel/pull/421) by [@VerdantFox](https://github.com/VerdantFox). ## 0.0.7 From ae144e0a39914ad5291d91028366501c3a1ab831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Kr=C3=BCger=20Svensson?= Date: Tue, 30 Aug 2022 18:18:32 +0200 Subject: [PATCH 141/906] =?UTF-8?q?=F0=9F=90=9B=20Fix=20auto=20detecting?= =?UTF-8?q?=20and=20setting=20`nullable`,=20allowing=20overrides=20in=20fi?= =?UTF-8?q?eld=20(#423)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Benjamin Rapaport Co-authored-by: Sebastián Ramírez --- sqlmodel/main.py | 9 +-- tests/test_nullable.py | 125 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 tests/test_nullable.py diff --git a/sqlmodel/main.py b/sqlmodel/main.py index a5ce8faf74..d343c698e9 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -423,11 +423,13 @@ def get_column_from_field(field: ModelField) -> Column: # type: ignore index = getattr(field.field_info, "index", Undefined) if index is Undefined: index = False + nullable = not primary_key and _is_field_noneable(field) + # Override derived nullability if the nullable property is set explicitly + # on the field if hasattr(field.field_info, "nullable"): field_nullable = getattr(field.field_info, "nullable") if field_nullable != Undefined: nullable = field_nullable - nullable = not primary_key and _is_field_nullable(field) args = [] foreign_key = getattr(field.field_info, "foreign_key", None) unique = getattr(field.field_info, "unique", False) @@ -644,11 +646,10 @@ def __tablename__(cls) -> str: return cls.__name__.lower() -def _is_field_nullable(field: ModelField) -> bool: +def _is_field_noneable(field: ModelField) -> bool: if not field.required: # Taken from [Pydantic](https://github.com/samuelcolvin/pydantic/blob/v1.8.2/pydantic/fields.py#L946-L947) - is_optional = field.allow_none and ( + return field.allow_none and ( field.shape != SHAPE_SINGLETON or not field.sub_fields ) - return is_optional and field.default is None and field.default_factory is None return False diff --git a/tests/test_nullable.py b/tests/test_nullable.py new file mode 100644 index 0000000000..1c8b37b218 --- /dev/null +++ b/tests/test_nullable.py @@ -0,0 +1,125 @@ +from typing import Optional + +import pytest +from sqlalchemy.exc import IntegrityError +from sqlmodel import Field, Session, SQLModel, create_engine + + +def test_nullable_fields(clear_sqlmodel, caplog): + class Hero(SQLModel, table=True): + primary_key: Optional[int] = Field( + default=None, + primary_key=True, + ) + required_value: str + optional_default_ellipsis: Optional[str] = Field(default=...) + optional_default_none: Optional[str] = Field(default=None) + optional_non_nullable: Optional[str] = Field( + nullable=False, + ) + optional_nullable: Optional[str] = Field( + nullable=True, + ) + optional_default_ellipses_non_nullable: Optional[str] = Field( + default=..., + nullable=False, + ) + optional_default_ellipses_nullable: Optional[str] = Field( + default=..., + nullable=True, + ) + optional_default_none_non_nullable: Optional[str] = Field( + default=None, + nullable=False, + ) + optional_default_none_nullable: Optional[str] = Field( + default=None, + nullable=True, + ) + default_ellipses_non_nullable: str = Field(default=..., nullable=False) + optional_default_str: Optional[str] = "default" + optional_default_str_non_nullable: Optional[str] = Field( + default="default", nullable=False + ) + optional_default_str_nullable: Optional[str] = Field( + default="default", nullable=True + ) + str_default_str: str = "default" + str_default_str_non_nullable: str = Field(default="default", nullable=False) + str_default_str_nullable: str = Field(default="default", nullable=True) + str_default_ellipsis_non_nullable: str = Field(default=..., nullable=False) + str_default_ellipsis_nullable: str = Field(default=..., nullable=True) + + engine = create_engine("sqlite://", echo=True) + SQLModel.metadata.create_all(engine) + + create_table_log = [ + message for message in caplog.messages if "CREATE TABLE hero" in message + ][0] + assert "primary_key INTEGER NOT NULL," in create_table_log + assert "required_value VARCHAR NOT NULL," in create_table_log + assert "optional_default_ellipsis VARCHAR NOT NULL," in create_table_log + assert "optional_default_none VARCHAR," in create_table_log + assert "optional_non_nullable VARCHAR NOT NULL," in create_table_log + assert "optional_nullable VARCHAR," in create_table_log + assert ( + "optional_default_ellipses_non_nullable VARCHAR NOT NULL," in create_table_log + ) + assert "optional_default_ellipses_nullable VARCHAR," in create_table_log + assert "optional_default_none_non_nullable VARCHAR NOT NULL," in create_table_log + assert "optional_default_none_nullable VARCHAR," in create_table_log + assert "default_ellipses_non_nullable VARCHAR NOT NULL," in create_table_log + assert "optional_default_str VARCHAR," in create_table_log + assert "optional_default_str_non_nullable VARCHAR NOT NULL," in create_table_log + assert "optional_default_str_nullable VARCHAR," in create_table_log + assert "str_default_str VARCHAR NOT NULL," in create_table_log + assert "str_default_str_non_nullable VARCHAR NOT NULL," in create_table_log + assert "str_default_str_nullable VARCHAR," in create_table_log + assert "str_default_ellipsis_non_nullable VARCHAR NOT NULL," in create_table_log + assert "str_default_ellipsis_nullable VARCHAR," in create_table_log + + +# Test for regression in https://github.com/tiangolo/sqlmodel/issues/420 +def test_non_nullable_optional_field_with_no_default_set(clear_sqlmodel, caplog): + class Hero(SQLModel, table=True): + primary_key: Optional[int] = Field( + default=None, + primary_key=True, + ) + + optional_non_nullable_no_default: Optional[str] = Field(nullable=False) + + engine = create_engine("sqlite://", echo=True) + SQLModel.metadata.create_all(engine) + + create_table_log = [ + message for message in caplog.messages if "CREATE TABLE hero" in message + ][0] + assert "primary_key INTEGER NOT NULL," in create_table_log + assert "optional_non_nullable_no_default VARCHAR NOT NULL," in create_table_log + + # We can create a hero with `None` set for the optional non-nullable field + hero = Hero(primary_key=123, optional_non_nullable_no_default=None) + # But we cannot commit it. + with Session(engine) as session: + session.add(hero) + with pytest.raises(IntegrityError): + session.commit() + + +def test_nullable_primary_key(clear_sqlmodel, caplog): + # Probably the weirdest corner case, it shouldn't happen anywhere, but let's test it + class Hero(SQLModel, table=True): + nullable_integer_primary_key: Optional[int] = Field( + default=None, + primary_key=True, + nullable=True, + ) + + engine = create_engine("sqlite://", echo=True) + SQLModel.metadata.create_all(engine) + + create_table_log = [ + message for message in caplog.messages if "CREATE TABLE hero" in message + ][0] + assert "nullable_integer_primary_key INTEGER," in create_table_log From fdb049bee33e6e906b9f424b32d63e701ccb74eb Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 30 Aug 2022 16:19:19 +0000 Subject: [PATCH 142/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 6e668f89c6..526177b13d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix auto detecting and setting `nullable`, allowing overrides in field. PR [#423](https://github.com/tiangolo/sqlmodel/pull/423) by [@JonasKs](https://github.com/JonasKs). * ♻️ Update `expresion.py`, sync from Jinja2 template, implement `inherit_cache` to solve errors like: `SAWarning: Class SelectOfScalar will not make use of SQL compilation caching`. PR [#422](https://github.com/tiangolo/sqlmodel/pull/422) by [@tiangolo](https://github.com/tiangolo). * ✏ Fix typo in `docs/tutorial/connect/remove-data-connections.md`. PR [#421](https://github.com/tiangolo/sqlmodel/pull/421) by [@VerdantFox](https://github.com/VerdantFox). From e88b5d3691a2b0f939cd301409780c4c2f50d5eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 30 Aug 2022 18:35:29 +0200 Subject: [PATCH 143/906] =?UTF-8?q?=F0=9F=93=9D=20Adjust=20and=20clarify?= =?UTF-8?q?=20docs=20for=20`docs/tutorial/create-db-and-table.md`=20(#426)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tutorial/create-db-and-table.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/tutorial/create-db-and-table.md b/docs/tutorial/create-db-and-table.md index 2bdecaac67..b81d309284 100644 --- a/docs/tutorial/create-db-and-table.md +++ b/docs/tutorial/create-db-and-table.md @@ -498,7 +498,7 @@ In this example it's just the `SQLModel.metadata.create_all(engine)`. Let's put it in a function `create_db_and_tables()`: -```Python hl_lines="22-23" +```Python hl_lines="19-20" {!./docs_src/tutorial/create_db_and_table/tutorial002.py[ln:1-20]!} # More code here later 👇 @@ -513,9 +513,9 @@ Let's put it in a function `create_db_and_tables()`: -If `SQLModel.metadata.create_all(engine)` was not in a function and we tried to import something from this module (from this file) in another, it would try to create the database and table **every time**. +If `SQLModel.metadata.create_all(engine)` was not in a function and we tried to import something from this module (from this file) in another, it would try to create the database and table **every time** we executed that other file that imported this module. -We don't want that to happen like that, only when we **intend** it to happen, that's why we put it in a function. +We don't want that to happen like that, only when we **intend** it to happen, that's why we put it in a function, because we can make sure that the tables are created only when we call that function, and not when this module is imported somewhere else. Now we would be able to, for example, import the `Hero` class in some other file without having those **side effects**. From a67326d3585b3d2029806c325657de18c27310d8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 30 Aug 2022 16:36:07 +0000 Subject: [PATCH 144/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 526177b13d..d0ef74784a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Adjust and clarify docs for `docs/tutorial/create-db-and-table.md`. PR [#426](https://github.com/tiangolo/sqlmodel/pull/426) by [@tiangolo](https://github.com/tiangolo). * 🐛 Fix auto detecting and setting `nullable`, allowing overrides in field. PR [#423](https://github.com/tiangolo/sqlmodel/pull/423) by [@JonasKs](https://github.com/JonasKs). * ♻️ Update `expresion.py`, sync from Jinja2 template, implement `inherit_cache` to solve errors like: `SAWarning: Class SelectOfScalar will not make use of SQL compilation caching`. PR [#422](https://github.com/tiangolo/sqlmodel/pull/422) by [@tiangolo](https://github.com/tiangolo). * ✏ Fix typo in `docs/tutorial/connect/remove-data-connections.md`. PR [#421](https://github.com/tiangolo/sqlmodel/pull/421) by [@VerdantFox](https://github.com/VerdantFox). From c94db7b8a088a966138e39aead3bb63e700e5bec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 30 Aug 2022 19:46:58 +0200 Subject: [PATCH 145/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index d0ef74784a..0fa25e9bba 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,9 +2,14 @@ ## Latest Changes -* 📝 Adjust and clarify docs for `docs/tutorial/create-db-and-table.md`. PR [#426](https://github.com/tiangolo/sqlmodel/pull/426) by [@tiangolo](https://github.com/tiangolo). +### Fixes + * 🐛 Fix auto detecting and setting `nullable`, allowing overrides in field. PR [#423](https://github.com/tiangolo/sqlmodel/pull/423) by [@JonasKs](https://github.com/JonasKs). * ♻️ Update `expresion.py`, sync from Jinja2 template, implement `inherit_cache` to solve errors like: `SAWarning: Class SelectOfScalar will not make use of SQL compilation caching`. PR [#422](https://github.com/tiangolo/sqlmodel/pull/422) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* 📝 Adjust and clarify docs for `docs/tutorial/create-db-and-table.md`. PR [#426](https://github.com/tiangolo/sqlmodel/pull/426) by [@tiangolo](https://github.com/tiangolo). * ✏ Fix typo in `docs/tutorial/connect/remove-data-connections.md`. PR [#421](https://github.com/tiangolo/sqlmodel/pull/421) by [@VerdantFox](https://github.com/VerdantFox). ## 0.0.7 From b3e1a66a21d91c86c91f1d23c2b8afc3c8c4c6c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 30 Aug 2022 19:47:41 +0200 Subject: [PATCH 146/906] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.0.?= =?UTF-8?q?8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 0fa25e9bba..3338520b35 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.0.8 + ### Fixes * 🐛 Fix auto detecting and setting `nullable`, allowing overrides in field. PR [#423](https://github.com/tiangolo/sqlmodel/pull/423) by [@JonasKs](https://github.com/JonasKs). From 75ce45588b6a13136916929be4c44946f7e00cbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 30 Aug 2022 19:47:41 +0200 Subject: [PATCH 147/906] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.0.?= =?UTF-8?q?8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index 8c57237115..720aa8c929 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.0.7" +__version__ = "0.0.8" # Re-export from SQLAlchemy from sqlalchemy.engine import create_mock_engine as create_mock_engine From 27a16744f2de1dbef31de40f220b940370347643 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 4 Nov 2022 21:04:31 +0100 Subject: [PATCH 148/906] =?UTF-8?q?=F0=9F=91=B7=20Update=20Dependabot=20co?= =?UTF-8?q?nfig=20(#484)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/dependabot.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 946f2358c0..cd972a0ba4 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,8 +5,12 @@ updates: directory: "/" schedule: interval: "daily" + commit-message: + prefix: ⬆ # Python - package-ecosystem: "pip" directory: "/" schedule: interval: "daily" + commit-message: + prefix: ⬆ From 94715b6fa8bf196156f5a519c4ba7b8591ad17e0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 4 Nov 2022 20:05:07 +0000 Subject: [PATCH 149/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 3338520b35..a113a88ca3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Update Dependabot config. PR [#484](https://github.com/tiangolo/sqlmodel/pull/484) by [@tiangolo](https://github.com/tiangolo). ## 0.0.8 From 8b87bf8b4067e546939d8efe4f1eec1859e83f14 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Nov 2022 21:43:01 +0100 Subject: [PATCH 150/906] =?UTF-8?q?=E2=AC=86=20Bump=20dawidd6/action-downl?= =?UTF-8?q?oad-artifact=20from=202.9.0=20to=202.24.0=20(#470)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 2.9.0 to 2.24.0. - [Release notes](https://github.com/dawidd6/action-download-artifact/releases) - [Commits](https://github.com/dawidd6/action-download-artifact/compare/v2.9.0...v2.24.0) --- updated-dependencies: - dependency-name: dawidd6/action-download-artifact dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/preview-docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml index e335e81f91..31b04a978e 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/preview-docs.yml @@ -12,7 +12,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Download Artifact Docs - uses: dawidd6/action-download-artifact@v2.9.0 + uses: dawidd6/action-download-artifact@v2.24.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-docs.yml From 1dde3e0044f489e341f906bb386c300970f03814 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Nov 2022 21:43:25 +0100 Subject: [PATCH 151/906] =?UTF-8?q?=E2=AC=86=20Bump=20actions/checkout=20f?= =?UTF-8?q?rom=202=20to=203.1.0=20(#458)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.1.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v3.1.0) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-docs.yml | 2 +- .github/workflows/latest-changes.yml | 2 +- .github/workflows/preview-docs.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/test.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 18e35b308e..1aead66865 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -19,7 +19,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v2 + - uses: actions/checkout@v3.1.0 - name: Set up Python uses: actions/setup-python@v2 with: diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml index 48fb6dc833..9c3edccbf3 100644 --- a/.github/workflows/latest-changes.yml +++ b/.github/workflows/latest-changes.yml @@ -20,7 +20,7 @@ jobs: latest-changes: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3.1.0 with: # To allow latest-changes to commit to the main branch token: ${{ secrets.ACTIONS_TOKEN }} diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml index 31b04a978e..a2a0097265 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/preview-docs.yml @@ -10,7 +10,7 @@ jobs: preview-docs: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3.1.0 - name: Download Artifact Docs uses: dawidd6/action-download-artifact@v2.24.0 with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 105dbdd4cc..046396ba32 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,7 +15,7 @@ jobs: publish: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3.1.0 - name: Set up Python uses: actions/setup-python@v2 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0d32926218..8af931454f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,7 +22,7 @@ jobs: fail-fast: false steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3.1.0 - name: Set up Python uses: actions/setup-python@v2 with: From 666a9a35577f1b4a3fa723f125fdf99b31f2586a Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 4 Nov 2022 20:43:32 +0000 Subject: [PATCH 152/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index a113a88ca3..c0e8b87d00 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump dawidd6/action-download-artifact from 2.9.0 to 2.24.0. PR [#470](https://github.com/tiangolo/sqlmodel/pull/470) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Update Dependabot config. PR [#484](https://github.com/tiangolo/sqlmodel/pull/484) by [@tiangolo](https://github.com/tiangolo). ## 0.0.8 From 375e83d06887ed60706ee8130d9123b58063b615 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Nov 2022 21:44:02 +0100 Subject: [PATCH 153/906] =?UTF-8?q?=E2=AC=86=20Update=20pytest=20requireme?= =?UTF-8?q?nt=20from=20^6.2.4=20to=20^7.0.1=20(#242)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates the requirements on [pytest](https://github.com/pytest-dev/pytest) to permit the latest version. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/6.2.4...7.0.1) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:development ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7f5e7f8037..1b9616169c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ pydantic = "^1.8.2" sqlalchemy2-stubs = {version = "*", allow-prereleases = true} [tool.poetry.dev-dependencies] -pytest = "^6.2.4" +pytest = "^7.0.1" mypy = "0.930" flake8 = "^3.9.2" black = {version = "^21.5-beta.1", python = "^3.7"} From 5615a80fc552be501a3af3eebf7ec8d94ef39a8e Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 4 Nov 2022 20:44:06 +0000 Subject: [PATCH 154/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index c0e8b87d00..0cd7753188 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump actions/checkout from 2 to 3.1.0. PR [#458](https://github.com/tiangolo/sqlmodel/pull/458) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump dawidd6/action-download-artifact from 2.9.0 to 2.24.0. PR [#470](https://github.com/tiangolo/sqlmodel/pull/470) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Update Dependabot config. PR [#484](https://github.com/tiangolo/sqlmodel/pull/484) by [@tiangolo](https://github.com/tiangolo). From ba76745f433ad03326ad393befadeba8e9854a61 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 4 Nov 2022 20:45:07 +0000 Subject: [PATCH 155/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 0cd7753188..50e88c7aa9 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Update pytest requirement from ^6.2.4 to ^7.0.1. PR [#242](https://github.com/tiangolo/sqlmodel/pull/242) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/checkout from 2 to 3.1.0. PR [#458](https://github.com/tiangolo/sqlmodel/pull/458) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump dawidd6/action-download-artifact from 2.9.0 to 2.24.0. PR [#470](https://github.com/tiangolo/sqlmodel/pull/470) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Update Dependabot config. PR [#484](https://github.com/tiangolo/sqlmodel/pull/484) by [@tiangolo](https://github.com/tiangolo). From 781174e6e9239445c7b7c700356f31c5f55a8d9c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Nov 2022 21:59:26 +0100 Subject: [PATCH 156/906] =?UTF-8?q?=E2=AC=86=20Update=20flake8=20requireme?= =?UTF-8?q?nt=20from=20^3.9.2=20to=20^5.0.4=20(#396)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates the requirements on [flake8](https://github.com/pycqa/flake8) to permit the latest version. - [Release notes](https://github.com/pycqa/flake8/releases) - [Commits](https://github.com/pycqa/flake8/compare/3.9.2...5.0.4) --- updated-dependencies: - dependency-name: flake8 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1b9616169c..addffbf4af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ sqlalchemy2-stubs = {version = "*", allow-prereleases = true} [tool.poetry.dev-dependencies] pytest = "^7.0.1" mypy = "0.930" -flake8 = "^3.9.2" +flake8 = "^5.0.4" black = {version = "^21.5-beta.1", python = "^3.7"} mkdocs = "^1.2.1" mkdocs-material = "^8.1.4" From e8f61fb9d0429e421664d398f5b39784ca805a24 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 4 Nov 2022 21:00:05 +0000 Subject: [PATCH 157/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 50e88c7aa9..026f83e506 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Update flake8 requirement from ^3.9.2 to ^5.0.4. PR [#396](https://github.com/tiangolo/sqlmodel/pull/396) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Update pytest requirement from ^6.2.4 to ^7.0.1. PR [#242](https://github.com/tiangolo/sqlmodel/pull/242) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/checkout from 2 to 3.1.0. PR [#458](https://github.com/tiangolo/sqlmodel/pull/458) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump dawidd6/action-download-artifact from 2.9.0 to 2.24.0. PR [#470](https://github.com/tiangolo/sqlmodel/pull/470) by [@dependabot[bot]](https://github.com/apps/dependabot). From fee7ecf61935d93007774c2d01536a9fdd4b5df5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Nov 2022 22:00:21 +0100 Subject: [PATCH 158/906] =?UTF-8?q?=E2=AC=86=20Bump=20actions/upload-artif?= =?UTF-8?q?act=20from=202=20to=203=20(#412)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 1aead66865..8a22d43a00 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -65,7 +65,7 @@ jobs: run: python -m poetry run mkdocs build --config-file mkdocs.insiders.yml - name: Zip docs run: python -m poetry run bash ./scripts/zip-docs.sh - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 with: name: docs-zip path: ./docs.zip From 592c877cfc7db29aa152e84a176ddf9e1bd53b66 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Nov 2022 22:00:58 +0100 Subject: [PATCH 159/906] =?UTF-8?q?=E2=AC=86=20Bump=20codecov/codecov-acti?= =?UTF-8?q?on=20from=202=20to=203=20(#415)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8af931454f..19cd691d86 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,4 +59,4 @@ jobs: - name: Test run: python -m poetry run bash scripts/test.sh - name: Upload coverage - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@v3 From ccdab8fb24373ba8c036ff3cdea7628fb74d1cea Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 4 Nov 2022 21:02:59 +0000 Subject: [PATCH 160/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 026f83e506..b999d0583e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump actions/upload-artifact from 2 to 3. PR [#412](https://github.com/tiangolo/sqlmodel/pull/412) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Update flake8 requirement from ^3.9.2 to ^5.0.4. PR [#396](https://github.com/tiangolo/sqlmodel/pull/396) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Update pytest requirement from ^6.2.4 to ^7.0.1. PR [#242](https://github.com/tiangolo/sqlmodel/pull/242) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/checkout from 2 to 3.1.0. PR [#458](https://github.com/tiangolo/sqlmodel/pull/458) by [@dependabot[bot]](https://github.com/apps/dependabot). From 91d8e382082ff546e52002c3acea02092098678d Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 4 Nov 2022 21:03:39 +0000 Subject: [PATCH 161/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index b999d0583e..331afd47e9 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump codecov/codecov-action from 2 to 3. PR [#415](https://github.com/tiangolo/sqlmodel/pull/415) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/upload-artifact from 2 to 3. PR [#412](https://github.com/tiangolo/sqlmodel/pull/412) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Update flake8 requirement from ^3.9.2 to ^5.0.4. PR [#396](https://github.com/tiangolo/sqlmodel/pull/396) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Update pytest requirement from ^6.2.4 to ^7.0.1. PR [#242](https://github.com/tiangolo/sqlmodel/pull/242) by [@dependabot[bot]](https://github.com/apps/dependabot). From f0f6f93e28f7a8cc58918edf949ebc483e943f10 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Nov 2022 22:13:58 +0100 Subject: [PATCH 162/906] =?UTF-8?q?=E2=AC=86=20Update=20coverage=20require?= =?UTF-8?q?ment=20from=20^5.5=20to=20^6.2=20(#171)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update coverage requirement from ^5.5 to ^6.2 Updates the requirements on [coverage](https://github.com/nedbat/coveragepy) to permit the latest version. - [Release notes](https://github.com/nedbat/coveragepy/releases) - [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst) - [Commits](https://github.com/nedbat/coveragepy/compare/coverage-5.5...6.2) --- updated-dependencies: - dependency-name: coverage dependency-type: direct:development ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index addffbf4af..3c501e5c6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ black = {version = "^21.5-beta.1", python = "^3.7"} mkdocs = "^1.2.1" mkdocs-material = "^8.1.4" mdx-include = "^1.4.1" -coverage = {extras = ["toml"], version = "^5.5"} +coverage = {extras = ["toml"], version = "^6.2"} fastapi = "^0.68.1" requests = "^2.26.0" autoflake = "^1.4" From 29d9721d1a3997860cfa96c99ebd278d2943c3b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Nov 2022 22:14:11 +0100 Subject: [PATCH 163/906] =?UTF-8?q?=E2=AC=86=20Update=20mypy=20requirement?= =?UTF-8?q?=20from=200.930=20to=200.971=20(#380)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update mypy requirement from 0.930 to 0.971 Updates the requirements on [mypy](https://github.com/python/mypy) to permit the latest version. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.930...v0.971) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:development ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3c501e5c6e..5796ac9c19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ sqlalchemy2-stubs = {version = "*", allow-prereleases = true} [tool.poetry.dev-dependencies] pytest = "^7.0.1" -mypy = "0.930" +mypy = "0.971" flake8 = "^5.0.4" black = {version = "^21.5-beta.1", python = "^3.7"} mkdocs = "^1.2.1" From b532a4c304239784b0c1af16aaa9bd37920bf3dc Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 4 Nov 2022 21:14:34 +0000 Subject: [PATCH 164/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 331afd47e9..4445685840 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Update coverage requirement from ^5.5 to ^6.2. PR [#171](https://github.com/tiangolo/sqlmodel/pull/171) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump codecov/codecov-action from 2 to 3. PR [#415](https://github.com/tiangolo/sqlmodel/pull/415) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/upload-artifact from 2 to 3. PR [#412](https://github.com/tiangolo/sqlmodel/pull/412) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Update flake8 requirement from ^3.9.2 to ^5.0.4. PR [#396](https://github.com/tiangolo/sqlmodel/pull/396) by [@dependabot[bot]](https://github.com/apps/dependabot). From 8003c7387748df659acfc5776e1a82e790e85bb6 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 4 Nov 2022 21:14:56 +0000 Subject: [PATCH 165/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 4445685840..b68ce79e5d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Update mypy requirement from 0.930 to 0.971. PR [#380](https://github.com/tiangolo/sqlmodel/pull/380) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Update coverage requirement from ^5.5 to ^6.2. PR [#171](https://github.com/tiangolo/sqlmodel/pull/171) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump codecov/codecov-action from 2 to 3. PR [#415](https://github.com/tiangolo/sqlmodel/pull/415) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/upload-artifact from 2 to 3. PR [#412](https://github.com/tiangolo/sqlmodel/pull/412) by [@dependabot[bot]](https://github.com/apps/dependabot). From c6ad5b810906be3ff05d1eb6344fffb11465bc7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 4 Nov 2022 22:16:59 +0100 Subject: [PATCH 166/906] =?UTF-8?q?=E2=9E=95=20Add=20extra=20dev=20depende?= =?UTF-8?q?ncies=20for=20MkDocs=20Material=20(#485)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 5796ac9c19..422cd8de6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,8 @@ flake8 = "^5.0.4" black = {version = "^21.5-beta.1", python = "^3.7"} mkdocs = "^1.2.1" mkdocs-material = "^8.1.4" +pillow = {version = "^9.3.0", python = "^3.7"} +cairosvg = {version = "^2.5.2", python = "^3.7"} mdx-include = "^1.4.1" coverage = {extras = ["toml"], version = "^6.2"} fastapi = "^0.68.1" From e7d8b69b530c0d35b63cb7e1271ea9ab9ee22ba1 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 4 Nov 2022 21:18:47 +0000 Subject: [PATCH 167/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index b68ce79e5d..e218bee4ad 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ➕ Add extra dev dependencies for MkDocs Material. PR [#485](https://github.com/tiangolo/sqlmodel/pull/485) by [@tiangolo](https://github.com/tiangolo). * ⬆ Update mypy requirement from 0.930 to 0.971. PR [#380](https://github.com/tiangolo/sqlmodel/pull/380) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Update coverage requirement from ^5.5 to ^6.2. PR [#171](https://github.com/tiangolo/sqlmodel/pull/171) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump codecov/codecov-action from 2 to 3. PR [#415](https://github.com/tiangolo/sqlmodel/pull/415) by [@dependabot[bot]](https://github.com/apps/dependabot). From 5ca9375f26a0716ad6bbc579f476ee85fdaa258e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Nov 2022 22:32:13 +0100 Subject: [PATCH 168/906] =?UTF-8?q?=E2=AC=86=20Update=20black=20requiremen?= =?UTF-8?q?t=20from=20^21.5-beta.1=20to=20^22.10.0=20(#460)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 422cd8de6f..3e0dd9b25a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ sqlalchemy2-stubs = {version = "*", allow-prereleases = true} pytest = "^7.0.1" mypy = "0.971" flake8 = "^5.0.4" -black = {version = "^21.5-beta.1", python = "^3.7"} +black = {version = "^22.10.0", python = "^3.7"} mkdocs = "^1.2.1" mkdocs-material = "^8.1.4" pillow = {version = "^9.3.0", python = "^3.7"} From bb32178bda15d55389ef201975f72edd35cb803f Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 4 Nov 2022 21:32:51 +0000 Subject: [PATCH 169/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index e218bee4ad..42f200f263 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Update black requirement from ^21.5-beta.1 to ^22.10.0. PR [#460](https://github.com/tiangolo/sqlmodel/pull/460) by [@dependabot[bot]](https://github.com/apps/dependabot). * ➕ Add extra dev dependencies for MkDocs Material. PR [#485](https://github.com/tiangolo/sqlmodel/pull/485) by [@tiangolo](https://github.com/tiangolo). * ⬆ Update mypy requirement from 0.930 to 0.971. PR [#380](https://github.com/tiangolo/sqlmodel/pull/380) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Update coverage requirement from ^5.5 to ^6.2. PR [#171](https://github.com/tiangolo/sqlmodel/pull/171) by [@dependabot[bot]](https://github.com/apps/dependabot). From 7e26dac198774d1db07505d72fb233e49c2d6978 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Nov 2022 22:43:45 +0100 Subject: [PATCH 170/906] =?UTF-8?q?=E2=AC=86=20Bump=20actions/setup-python?= =?UTF-8?q?=20from=202=20to=204=20(#411)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- .github/workflows/build-docs.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/test.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 8a22d43a00..b518a988f7 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -21,7 +21,7 @@ jobs: run: echo "$GITHUB_CONTEXT" - uses: actions/checkout@v3.1.0 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: "3.7" # Allow debugging with tmate diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 046396ba32..f42c6447f6 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -17,7 +17,7 @@ jobs: steps: - uses: actions/checkout@v3.1.0 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: "3.7" # Allow debugging with tmate diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 19cd691d86..bceec7e71f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,7 +24,7 @@ jobs: steps: - uses: actions/checkout@v3.1.0 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} # Allow debugging with tmate From ea79c47c0ed34fc878600c3eaad6909106a0cf4d Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 4 Nov 2022 21:44:25 +0000 Subject: [PATCH 171/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 42f200f263..8777a8fce4 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump actions/setup-python from 2 to 4. PR [#411](https://github.com/tiangolo/sqlmodel/pull/411) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Update black requirement from ^21.5-beta.1 to ^22.10.0. PR [#460](https://github.com/tiangolo/sqlmodel/pull/460) by [@dependabot[bot]](https://github.com/apps/dependabot). * ➕ Add extra dev dependencies for MkDocs Material. PR [#485](https://github.com/tiangolo/sqlmodel/pull/485) by [@tiangolo](https://github.com/tiangolo). * ⬆ Update mypy requirement from 0.930 to 0.971. PR [#380](https://github.com/tiangolo/sqlmodel/pull/380) by [@dependabot[bot]](https://github.com/apps/dependabot). From 595694090839bd26495adbce42dc422ef63cb694 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 4 Nov 2022 23:01:37 +0100 Subject: [PATCH 172/906] =?UTF-8?q?=F0=9F=91=B7=20Move=20from=20Codecov=20?= =?UTF-8?q?to=20Smokeshow=20(#486)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/smokeshow.yml | 35 +++++++++++++++++++++++++++++ .github/workflows/test.yml | 40 +++++++++++++++++++++++++++++++-- README.md | 5 ++--- docs/index.md | 5 ++--- pyproject.toml | 1 + scripts/test-cov-html.sh | 7 ------ scripts/test.sh | 2 +- 7 files changed, 79 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/smokeshow.yml delete mode 100755 scripts/test-cov-html.sh diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml new file mode 100644 index 0000000000..606633a99d --- /dev/null +++ b/.github/workflows/smokeshow.yml @@ -0,0 +1,35 @@ +name: Smokeshow + +on: + workflow_run: + workflows: [Test] + types: [completed] + +permissions: + statuses: write + +jobs: + smokeshow: + if: ${{ github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest + + steps: + - uses: actions/setup-python@v4 + with: + python-version: '3.9' + + - run: pip install smokeshow + + - uses: dawidd6/action-download-artifact@v2 + with: + workflow: test.yml + commit: ${{ github.event.workflow_run.head_sha }} + + - run: smokeshow upload coverage-html + env: + SMOKESHOW_GITHUB_STATUS_DESCRIPTION: Coverage {coverage-percentage} + SMOKESHOW_GITHUB_COVERAGE_THRESHOLD: 100 + SMOKESHOW_GITHUB_CONTEXT: coverage + SMOKESHOW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SMOKESHOW_GITHUB_PR_HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + SMOKESHOW_AUTH_KEY: ${{ secrets.SMOKESHOW_AUTH_KEY }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bceec7e71f..03c55df422 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -56,7 +56,43 @@ jobs: - name: Lint if: ${{ matrix.python-version != '3.6.15' }} run: python -m poetry run bash scripts/lint.sh + - run: mkdir coverage - name: Test run: python -m poetry run bash scripts/test.sh - - name: Upload coverage - uses: codecov/codecov-action@v3 + env: + COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }} + CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }} + - name: Store coverage files + uses: actions/upload-artifact@v3 + with: + name: coverage + path: coverage + coverage-combine: + needs: [test] + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-python@v4 + with: + python-version: '3.8' + + - name: Get coverage files + uses: actions/download-artifact@v3 + with: + name: coverage + path: coverage + + - run: pip install coverage[toml] + + - run: ls -la coverage + - run: coverage combine coverage + - run: coverage report + - run: coverage html --show-contexts --title "Coverage for ${{ github.sha }}" + + - name: Store coverage HTML + uses: actions/upload-artifact@v3 + with: + name: coverage-html + path: htmlcov diff --git a/README.md b/README.md index 5a63c9da44..5721f1cdb0 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,8 @@ Publish - - Coverage - + + Coverage Package version diff --git a/docs/index.md b/docs/index.md index 5a63c9da44..5721f1cdb0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,9 +11,8 @@ Publish - - Coverage - + + Coverage Package version diff --git a/pyproject.toml b/pyproject.toml index 3e0dd9b25a..e3b1d3c279 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,6 +67,7 @@ source = [ "tests", "sqlmodel" ] +context = '${CONTEXT}' [tool.coverage.report] exclude_lines = [ diff --git a/scripts/test-cov-html.sh b/scripts/test-cov-html.sh deleted file mode 100755 index b15445faaa..0000000000 --- a/scripts/test-cov-html.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -set -e -set -x - -bash ./scripts/test.sh -coverage html diff --git a/scripts/test.sh b/scripts/test.sh index 7fce865bd6..9b758bdbdf 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -6,4 +6,4 @@ set -x coverage run -m pytest tests coverage combine coverage report --show-missing -coverage xml +coverage html From f0aa199611610e5188ad4b9223719a61c566b3a2 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 4 Nov 2022 22:02:09 +0000 Subject: [PATCH 173/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 8777a8fce4..5794fd0f8b 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Move from Codecov to Smokeshow. PR [#486](https://github.com/tiangolo/sqlmodel/pull/486) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump actions/setup-python from 2 to 4. PR [#411](https://github.com/tiangolo/sqlmodel/pull/411) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Update black requirement from ^21.5-beta.1 to ^22.10.0. PR [#460](https://github.com/tiangolo/sqlmodel/pull/460) by [@dependabot[bot]](https://github.com/apps/dependabot). * ➕ Add extra dev dependencies for MkDocs Material. PR [#485](https://github.com/tiangolo/sqlmodel/pull/485) by [@tiangolo](https://github.com/tiangolo). From 8070b142b8a6bef63644d4d0e6426f72b13e798f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 5 Nov 2022 02:04:13 +0100 Subject: [PATCH 174/906] =?UTF-8?q?=F0=9F=94=A7=20Update=20Smokeshow=20cov?= =?UTF-8?q?erage=20threshold=20(#487)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/smokeshow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index 606633a99d..5242cc7218 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -28,7 +28,7 @@ jobs: - run: smokeshow upload coverage-html env: SMOKESHOW_GITHUB_STATUS_DESCRIPTION: Coverage {coverage-percentage} - SMOKESHOW_GITHUB_COVERAGE_THRESHOLD: 100 + SMOKESHOW_GITHUB_COVERAGE_THRESHOLD: 95 SMOKESHOW_GITHUB_CONTEXT: coverage SMOKESHOW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SMOKESHOW_GITHUB_PR_HEAD_SHA: ${{ github.event.workflow_run.head_sha }} From 3a9244c69f7a71a1d676927d052384a84d938018 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 5 Nov 2022 01:04:44 +0000 Subject: [PATCH 175/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 5794fd0f8b..a690087e12 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update Smokeshow coverage threshold. PR [#487](https://github.com/tiangolo/sqlmodel/pull/487) by [@tiangolo](https://github.com/tiangolo). * 👷 Move from Codecov to Smokeshow. PR [#486](https://github.com/tiangolo/sqlmodel/pull/486) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump actions/setup-python from 2 to 4. PR [#411](https://github.com/tiangolo/sqlmodel/pull/411) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Update black requirement from ^21.5-beta.1 to ^22.10.0. PR [#460](https://github.com/tiangolo/sqlmodel/pull/460) by [@dependabot[bot]](https://github.com/apps/dependabot). From 22faa192b936e9a71c61e2da4ae1350cc00f087a Mon Sep 17 00:00:00 2001 From: Michael Cuffaro Date: Mon, 7 Nov 2022 15:21:23 +0100 Subject: [PATCH 176/906] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20small=20typo?= =?UTF-8?q?s=20in=20docs=20(#481)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix small typos Fixing small typos here and there while reading the document. Co-authored-by: Sebastián Ramírez --- docs/tutorial/where.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/tutorial/where.md b/docs/tutorial/where.md index d4e4639dba..ca85a4dd00 100644 --- a/docs/tutorial/where.md +++ b/docs/tutorial/where.md @@ -311,7 +311,7 @@ Instead, it results in a special type of object. If you tried that in an interac ``` -So, that result value is an **expession** object. 💡 +So, that result value is an **expression** object. 💡 And `.where()` takes one (or more) of these **expression** objects to update the SQL statement. @@ -421,7 +421,7 @@ Of course, the keyword arguments would have been a bit shorter. But with the **expressions** your editor can help you a lot with autocompletion and inline error checks. ✨ -Let me give you an example. Let's imagine that keword arguments were supported in SQLModel and you wanted to filter using the secret identity of Spider-Boy. +Let me give you an example. Let's imagine that keyword arguments were supported in SQLModel and you wanted to filter using the secret identity of Spider-Boy. You could write: @@ -436,7 +436,7 @@ Maybe your code could even run and seem like it's all fine, and then some months And maybe finally you would realize that we wrote the code using `secret_identity` which is not a column in the table. We should have written `secret_name` instead. -Now, with the the expressions, your editor would show you an error right away if you tried this: +Now, with the expressions, your editor would show you an error right away if you tried this: ```Python # Expression ✨ @@ -694,7 +694,7 @@ age=35 id=5 name='Black Lion' secret_name='Trevor Challa' !!! tip We get `Black Lion` here too because although the age is not *strictly* less than `35` it is *equal* to `35`. -### Benefits of Expresions +### Benefits of Expressions Here's a good moment to see that being able to use these pure Python expressions instead of keyword arguments can help a lot. ✨ From fbc6f3b53620fc16e418c3e5b3a4e387058a077a Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 7 Nov 2022 14:21:59 +0000 Subject: [PATCH 177/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index a690087e12..c3332f3423 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏️ Fix small typos in docs. PR [#481](https://github.com/tiangolo/sqlmodel/pull/481) by [@micuffaro](https://github.com/micuffaro). * 🔧 Update Smokeshow coverage threshold. PR [#487](https://github.com/tiangolo/sqlmodel/pull/487) by [@tiangolo](https://github.com/tiangolo). * 👷 Move from Codecov to Smokeshow. PR [#486](https://github.com/tiangolo/sqlmodel/pull/486) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump actions/setup-python from 2 to 4. PR [#411](https://github.com/tiangolo/sqlmodel/pull/411) by [@dependabot[bot]](https://github.com/apps/dependabot). From 209791734c9573283e1ed6ea1dd5d25dfeb2b872 Mon Sep 17 00:00:00 2001 From: Santhosh Solomon <91061721+FluffyDietEngine@users.noreply.github.com> Date: Tue, 8 Nov 2022 20:34:06 +0530 Subject: [PATCH 178/906] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo=20in=20?= =?UTF-8?q?`docs/tutorial/create-db-and-table.md`=20(#477)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit typo fixed in docs/tutorial/create-db-and-table.md Co-authored-by: Santhosh Solomon Co-authored-by: Sebastián Ramírez --- docs/tutorial/create-db-and-table.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/create-db-and-table.md b/docs/tutorial/create-db-and-table.md index b81d309284..abd73cb797 100644 --- a/docs/tutorial/create-db-and-table.md +++ b/docs/tutorial/create-db-and-table.md @@ -415,7 +415,7 @@ Now run the program with Python: // We set echo=True, so this will show the SQL code $ python app.py -// First, some boilerplate SQL that we are not that intereted in +// First, some boilerplate SQL that we are not that interested in INFO Engine BEGIN (implicit) INFO Engine PRAGMA main.table_info("hero") From 06bb369bcb6eff7eb8c7c60a47f327751f40a4db Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 8 Nov 2022 15:04:40 +0000 Subject: [PATCH 179/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index c3332f3423..63c056353b 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏️ Fix typo in `docs/tutorial/create-db-and-table.md`. PR [#477](https://github.com/tiangolo/sqlmodel/pull/477) by [@FluffyDietEngine](https://github.com/FluffyDietEngine). * ✏️ Fix small typos in docs. PR [#481](https://github.com/tiangolo/sqlmodel/pull/481) by [@micuffaro](https://github.com/micuffaro). * 🔧 Update Smokeshow coverage threshold. PR [#487](https://github.com/tiangolo/sqlmodel/pull/487) by [@tiangolo](https://github.com/tiangolo). * 👷 Move from Codecov to Smokeshow. PR [#486](https://github.com/tiangolo/sqlmodel/pull/486) by [@tiangolo](https://github.com/tiangolo). From 203db14e9b4b1cc73c6208b7beef7581755fa166 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Nov 2022 07:43:06 +0100 Subject: [PATCH 180/906] =?UTF-8?q?=E2=AC=86=20Bump=20dawidd6/action-downl?= =?UTF-8?q?oad-artifact=20from=202.24.0=20to=202.24.2=20(#493)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 2.24.0 to 2.24.2. - [Release notes](https://github.com/dawidd6/action-download-artifact/releases) - [Commits](https://github.com/dawidd6/action-download-artifact/compare/v2.24.0...v2.24.2) --- updated-dependencies: - dependency-name: dawidd6/action-download-artifact dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/preview-docs.yml | 2 +- .github/workflows/smokeshow.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml index a2a0097265..10e44328d6 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/preview-docs.yml @@ -12,7 +12,7 @@ jobs: steps: - uses: actions/checkout@v3.1.0 - name: Download Artifact Docs - uses: dawidd6/action-download-artifact@v2.24.0 + uses: dawidd6/action-download-artifact@v2.24.2 with: github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-docs.yml diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index 5242cc7218..7ee17cac55 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -20,7 +20,7 @@ jobs: - run: pip install smokeshow - - uses: dawidd6/action-download-artifact@v2 + - uses: dawidd6/action-download-artifact@v2.24.2 with: workflow: test.yml commit: ${{ github.event.workflow_run.head_sha }} From c31bac638c098d279425b7c59a16d156c2f089a5 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 10 Nov 2022 06:43:44 +0000 Subject: [PATCH 181/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 63c056353b..e71d4467c1 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump dawidd6/action-download-artifact from 2.24.0 to 2.24.2. PR [#493](https://github.com/tiangolo/sqlmodel/pull/493) by [@dependabot[bot]](https://github.com/apps/dependabot). * ✏️ Fix typo in `docs/tutorial/create-db-and-table.md`. PR [#477](https://github.com/tiangolo/sqlmodel/pull/477) by [@FluffyDietEngine](https://github.com/FluffyDietEngine). * ✏️ Fix small typos in docs. PR [#481](https://github.com/tiangolo/sqlmodel/pull/481) by [@micuffaro](https://github.com/micuffaro). * 🔧 Update Smokeshow coverage threshold. PR [#487](https://github.com/tiangolo/sqlmodel/pull/487) by [@tiangolo](https://github.com/tiangolo). From 54b5db98427871255df7041316babaec767cdf1f Mon Sep 17 00:00:00 2001 From: David Brochart Date: Fri, 11 Nov 2022 18:30:09 +0100 Subject: [PATCH 182/906] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo=20in=20?= =?UTF-8?q?docs=20(#446)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix typo * Again Co-authored-by: Sebastián Ramírez --- docs/db-to-code.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/db-to-code.md b/docs/db-to-code.md index ce9ffac251..2e0fb1babc 100644 --- a/docs/db-to-code.md +++ b/docs/db-to-code.md @@ -62,7 +62,7 @@ The user is probably, in some way, telling your application: 2 ``` -And the would be this table (with a single row): +And the result would be this table (with a single row):
From 0fe1e6d5a3f2703b02a323e8f8481ffe3096cfe2 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 11 Nov 2022 17:30:42 +0000 Subject: [PATCH 183/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index e71d4467c1..7e2383d65a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏️ Fix typo in docs. PR [#446](https://github.com/tiangolo/sqlmodel/pull/446) by [@davidbrochart](https://github.com/davidbrochart). * ⬆ Bump dawidd6/action-download-artifact from 2.24.0 to 2.24.2. PR [#493](https://github.com/tiangolo/sqlmodel/pull/493) by [@dependabot[bot]](https://github.com/apps/dependabot). * ✏️ Fix typo in `docs/tutorial/create-db-and-table.md`. PR [#477](https://github.com/tiangolo/sqlmodel/pull/477) by [@FluffyDietEngine](https://github.com/FluffyDietEngine). * ✏️ Fix small typos in docs. PR [#481](https://github.com/tiangolo/sqlmodel/pull/481) by [@micuffaro](https://github.com/micuffaro). From aa87ff37ea5cb758015d241e3aef994995d12fad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 12 Nov 2022 07:37:34 +0100 Subject: [PATCH 184/906] =?UTF-8?q?=E2=AC=86=20Bump=20actions/cache=20from?= =?UTF-8?q?=202=20to=203=20(#497)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/cache](https://github.com/actions/cache) from 2 to 3. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-docs.yml | 4 ++-- .github/workflows/publish.yml | 2 +- .github/workflows/test.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index b518a988f7..0d92d1feb3 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -30,7 +30,7 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }} with: limit-access-to-actor: true - - uses: actions/cache@v2 + - uses: actions/cache@v3 id: cache with: path: ${{ env.pythonLocation }} @@ -53,7 +53,7 @@ jobs: - name: Install Material for MkDocs Insiders if: github.event.pull_request.head.repo.fork == false && steps.cache.outputs.cache-hit != 'true' run: python -m poetry run pip install git+https://${{ secrets.ACTIONS_TOKEN }}@github.com/squidfunk/mkdocs-material-insiders.git - - uses: actions/cache@v2 + - uses: actions/cache@v3 with: key: mkdocs-cards-${{ github.ref }} path: .cache diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f42c6447f6..f3c1e980a6 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -26,7 +26,7 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }} with: limit-access-to-actor: true - - uses: actions/cache@v2 + - uses: actions/cache@v3 id: cache with: path: ${{ env.pythonLocation }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 03c55df422..585ffc0455 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,7 +33,7 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }} with: limit-access-to-actor: true - - uses: actions/cache@v2 + - uses: actions/cache@v3 id: cache with: path: ${{ env.pythonLocation }} From 497270dc552312120e436cb222e2391b4995f2fc Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 12 Nov 2022 06:38:16 +0000 Subject: [PATCH 185/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 7e2383d65a..0c8d5fe398 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump actions/cache from 2 to 3. PR [#497](https://github.com/tiangolo/sqlmodel/pull/497) by [@dependabot[bot]](https://github.com/apps/dependabot). * ✏️ Fix typo in docs. PR [#446](https://github.com/tiangolo/sqlmodel/pull/446) by [@davidbrochart](https://github.com/davidbrochart). * ⬆ Bump dawidd6/action-download-artifact from 2.24.0 to 2.24.2. PR [#493](https://github.com/tiangolo/sqlmodel/pull/493) by [@dependabot[bot]](https://github.com/apps/dependabot). * ✏️ Fix typo in `docs/tutorial/create-db-and-table.md`. PR [#477](https://github.com/tiangolo/sqlmodel/pull/477) by [@FluffyDietEngine](https://github.com/FluffyDietEngine). From 267cd42fb6c17b43a8edb738da1b689af6909300 Mon Sep 17 00:00:00 2001 From: Colin Marquardt Date: Sat, 12 Nov 2022 07:44:19 +0100 Subject: [PATCH 186/906] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo=20in=20?= =?UTF-8?q?internal=20function=20name=20`get=5Fsqlachemy=5Ftype()`=20(#496?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Corrected name is get_sqlalchemy_type(). --- sqlmodel/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index d343c698e9..d95c498507 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -371,7 +371,7 @@ def __init__( ModelMetaclass.__init__(cls, classname, bases, dict_, **kw) -def get_sqlachemy_type(field: ModelField) -> Any: +def get_sqlalchemy_type(field: ModelField) -> Any: if issubclass(field.type_, str): if field.field_info.max_length: return AutoString(length=field.field_info.max_length) @@ -418,7 +418,7 @@ def get_column_from_field(field: ModelField) -> Column: # type: ignore sa_column = getattr(field.field_info, "sa_column", Undefined) if isinstance(sa_column, Column): return sa_column - sa_type = get_sqlachemy_type(field) + sa_type = get_sqlalchemy_type(field) primary_key = getattr(field.field_info, "primary_key", False) index = getattr(field.field_info, "index", Undefined) if index is Undefined: From 5fa9062ed94b2be090ef9d15ea6666d347d1a2a6 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 12 Nov 2022 06:44:52 +0000 Subject: [PATCH 187/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 0c8d5fe398..f7c2f559b8 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏️ Fix typo in internal function name `get_sqlachemy_type()`. PR [#496](https://github.com/tiangolo/sqlmodel/pull/496) by [@cmarqu](https://github.com/cmarqu). * ⬆ Bump actions/cache from 2 to 3. PR [#497](https://github.com/tiangolo/sqlmodel/pull/497) by [@dependabot[bot]](https://github.com/apps/dependabot). * ✏️ Fix typo in docs. PR [#446](https://github.com/tiangolo/sqlmodel/pull/446) by [@davidbrochart](https://github.com/davidbrochart). * ⬆ Bump dawidd6/action-download-artifact from 2.24.0 to 2.24.2. PR [#493](https://github.com/tiangolo/sqlmodel/pull/493) by [@dependabot[bot]](https://github.com/apps/dependabot). From cf36b2d9baccf527bc61071850f102e2cd8bf6bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 16 Dec 2022 22:45:51 +0400 Subject: [PATCH 188/906] =?UTF-8?q?=F0=9F=91=B7=20Refactor=20CI=20artifact?= =?UTF-8?q?=20upload/download=20for=20docs=20previews=20(#514)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 2 +- .github/workflows/preview-docs.yml | 7 ++++++- scripts/zip-docs.sh | 4 +++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 0d92d1feb3..6400691533 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -68,7 +68,7 @@ jobs: - uses: actions/upload-artifact@v3 with: name: docs-zip - path: ./docs.zip + path: ./site/docs.zip - name: Deploy to Netlify uses: nwtgck/actions-netlify@v1.1.5 with: diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml index 10e44328d6..3550a9b441 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/preview-docs.yml @@ -11,6 +11,10 @@ jobs: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v3.1.0 + - name: Clean site + run: | + rm -rf ./site + mkdir ./site - name: Download Artifact Docs uses: dawidd6/action-download-artifact@v2.24.2 with: @@ -18,9 +22,10 @@ jobs: workflow: build-docs.yml run_id: ${{ github.event.workflow_run.id }} name: docs-zip + path: ./site/ - name: Unzip docs run: | - rm -rf ./site + cd ./site unzip docs.zip rm -f docs.zip - name: Deploy to Netlify diff --git a/scripts/zip-docs.sh b/scripts/zip-docs.sh index f2b7ba3be3..69315f5ddd 100644 --- a/scripts/zip-docs.sh +++ b/scripts/zip-docs.sh @@ -3,7 +3,9 @@ set -x set -e +cd ./site + if [ -f docs.zip ]; then rm -rf docs.zip fi -zip -r docs.zip ./site +zip -r docs.zip ./ From 7b3148c0b4bba173710c774c951cee89dcc95c39 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 16 Dec 2022 18:46:26 +0000 Subject: [PATCH 189/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index f7c2f559b8..4a4788f3bd 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Refactor CI artifact upload/download for docs previews. PR [#514](https://github.com/tiangolo/sqlmodel/pull/514) by [@tiangolo](https://github.com/tiangolo). * ✏️ Fix typo in internal function name `get_sqlachemy_type()`. PR [#496](https://github.com/tiangolo/sqlmodel/pull/496) by [@cmarqu](https://github.com/cmarqu). * ⬆ Bump actions/cache from 2 to 3. PR [#497](https://github.com/tiangolo/sqlmodel/pull/497) by [@dependabot[bot]](https://github.com/apps/dependabot). * ✏️ Fix typo in docs. PR [#446](https://github.com/tiangolo/sqlmodel/pull/446) by [@davidbrochart](https://github.com/davidbrochart). From 624a2142bf8779ae734621260613a187aebc274c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 30 Jan 2023 11:49:56 +0100 Subject: [PATCH 190/906] =?UTF-8?q?=F0=9F=94=A7=20Add=20template=20for=20G?= =?UTF-8?q?itHub=20Discussion=20questions=20and=20update=20issues=20templa?= =?UTF-8?q?te=20(#544)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/DISCUSSION_TEMPLATE/questions.yml | 160 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/question.yml | 20 +-- 2 files changed, 170 insertions(+), 10 deletions(-) create mode 100644 .github/DISCUSSION_TEMPLATE/questions.yml diff --git a/.github/DISCUSSION_TEMPLATE/questions.yml b/.github/DISCUSSION_TEMPLATE/questions.yml new file mode 100644 index 0000000000..97902a658e --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/questions.yml @@ -0,0 +1,160 @@ +labels: [question] +body: + - type: markdown + attributes: + value: | + Thanks for your interest in SQLModel! 🚀 + + Please follow these instructions, fill every question, and do every step. 🙏 + + I'm asking this because answering questions and solving problems in GitHub is what consumes most of the time. + + I end up not being able to add new features, fix bugs, review pull requests, etc. as fast as I wish because I have to spend too much time handling questions. + + All that, on top of all the incredible help provided by a bunch of community members that give a lot of their time to come here and help others. + + If more SQLModel users came to help others like them just a little bit more, it would be much less effort for them (and you and me 😅). + + By asking questions in a structured way (following this) it will be much easier to help you. + + And there's a high chance that you will find the solution along the way and you won't even have to submit it and wait for an answer. 😎 + + As there are too many questions, I'll have to discard and close the incomplete ones. That will allow me (and others) to focus on helping people like you that follow the whole process and help us help you. 🤓 + - type: checkboxes + id: checks + attributes: + label: First Check + description: Please confirm and check all the following options. + options: + - label: I added a very descriptive title here. + required: true + - label: I used the GitHub search to find a similar question and didn't find it. + required: true + - label: I searched the SQLModel documentation, with the integrated search. + required: true + - label: I already searched in Google "How to X in SQLModel" and didn't find any information. + required: true + - label: I already read and followed all the tutorial in the docs and didn't find an answer. + required: true + - label: I already checked if it is not related to SQLModel but to [Pydantic](https://github.com/samuelcolvin/pydantic). + required: true + - label: I already checked if it is not related to SQLModel but to [SQLAlchemy](https://github.com/sqlalchemy/sqlalchemy). + required: true + - type: checkboxes + id: help + attributes: + label: Commit to Help + description: | + After submitting this, I commit to one of: + + * Read open issues with questions until I find 2 issues where I can help someone and add a comment to help there. + * I already hit the "watch" button in this repository to receive notifications and I commit to help at least 2 people that ask questions in the future. + * Review one Pull Request by downloading the code and following all the review process](https://sqlmodel.tiangolo.com/help/#review-pull-requests). + + options: + - label: I commit to help with one of those options 👆 + required: true + - type: textarea + id: example + attributes: + label: Example Code + description: | + Please add a self-contained, [minimal, reproducible, example](https://stackoverflow.com/help/minimal-reproducible-example) with your use case. + + If I (or someone) can copy it, run it, and see it right away, there's a much higher chance I (or someone) will be able to help you. + + placeholder: | + from typing import Optional + + from sqlmodel import Field, Session, SQLModel, create_engine + + + class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str + secret_name: str + age: Optional[int] = None + + + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + + engine = create_engine("sqlite:///database.db") + + + SQLModel.metadata.create_all(engine) + + with Session(engine) as session: + session.add(hero_1) + session.commit() + session.refresh(hero_1) + print(hero_1) + render: python + validations: + required: true + - type: textarea + id: description + attributes: + label: Description + description: | + What is the problem, question, or error? + + Write a short description telling me what you are doing, what you expect to happen, and what is currently happening. + placeholder: | + * Create a Hero model. + * Create a Hero instance. + * Save it to a SQLite database. + * Check the data with DB Browser for SQLite. + * There's only one hero there, but I expected it to include many others recruited by the first one. + validations: + required: true + - type: dropdown + id: os + attributes: + label: Operating System + description: What operating system are you on? + multiple: true + options: + - Linux + - Windows + - macOS + - Other + validations: + required: true + - type: textarea + id: os-details + attributes: + label: Operating System Details + description: You can add more details about your operating system here, in particular if you chose "Other". + - type: input + id: sqlmodel-version + attributes: + label: SQLModel Version + description: | + What SQLModel version are you using? + + You can find the SQLModel version with: + + ```bash + python -c "import sqlmodel; print(sqlmodel.__version__)" + ``` + validations: + required: true + - type: input + id: python-version + attributes: + label: Python Version + description: | + What Python version are you using? + + You can find the Python version with: + + ```bash + python --version + ``` + validations: + required: true + - type: textarea + id: context + attributes: + label: Additional Context + description: Add any additional context information or screenshots you think are useful. diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml index a57446755e..dfdc25dc89 100644 --- a/.github/ISSUE_TEMPLATE/question.yml +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -8,29 +8,29 @@ body: Thanks for your interest in SQLModel! 🚀 Please follow these instructions, fill every question, and do every step. 🙏 - - I'm asking this because answering questions and solving problems in GitHub issues is what consumes most of the time. - - I end up not being able to add new features, fix bugs, review pull requests, etc. as fast as I wish because I have to spend too much time handling issues. + + I'm asking this because answering questions and solving problems in GitHub is what consumes most of the time. + + I end up not being able to add new features, fix bugs, review pull requests, etc. as fast as I wish because I have to spend too much time handling questions. All that, on top of all the incredible help provided by a bunch of community members that give a lot of their time to come here and help others. If more SQLModel users came to help others like them just a little bit more, it would be much less effort for them (and you and me 😅). By asking questions in a structured way (following this) it will be much easier to help you. - + And there's a high chance that you will find the solution along the way and you won't even have to submit it and wait for an answer. 😎 - As there are too many issues with questions, I'll have to close the incomplete ones. That will allow me (and others) to focus on helping people like you that follow the whole process and help us help you. 🤓 + As there are too many questions, I'll have to discard and close the incomplete ones. That will allow me (and others) to focus on helping people like you that follow the whole process and help us help you. 🤓 - type: checkboxes id: checks attributes: label: First Check description: Please confirm and check all the following options. options: - - label: I added a very descriptive title to this issue. + - label: I added a very descriptive title here. required: true - - label: I used the GitHub search to find a similar issue and didn't find it. + - label: I used the GitHub search to find a similar question and didn't find it. required: true - label: I searched the SQLModel documentation, with the integrated search. required: true @@ -48,10 +48,10 @@ body: label: Commit to Help description: | After submitting this, I commit to one of: - + * Read open issues with questions until I find 2 issues where I can help someone and add a comment to help there. * I already hit the "watch" button in this repository to receive notifications and I commit to help at least 2 people that ask questions in the future. - * Implement a Pull Request for a confirmed bug. + * Review one Pull Request by downloading the code and following all the review process](https://sqlmodel.tiangolo.com/help/#review-pull-requests). options: - label: I commit to help with one of those options 👆 From c47a54df918191fda75dd5f3a397f29740f7419a Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 30 Jan 2023 10:50:32 +0000 Subject: [PATCH 191/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 4a4788f3bd..9a680bcd07 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Add template for GitHub Discussion questions and update issues template. PR [#544](https://github.com/tiangolo/sqlmodel/pull/544) by [@tiangolo](https://github.com/tiangolo). * 👷 Refactor CI artifact upload/download for docs previews. PR [#514](https://github.com/tiangolo/sqlmodel/pull/514) by [@tiangolo](https://github.com/tiangolo). * ✏️ Fix typo in internal function name `get_sqlachemy_type()`. PR [#496](https://github.com/tiangolo/sqlmodel/pull/496) by [@cmarqu](https://github.com/cmarqu). * ⬆ Bump actions/cache from 2 to 3. PR [#497](https://github.com/tiangolo/sqlmodel/pull/497) by [@dependabot[bot]](https://github.com/apps/dependabot). From 1c294ddeb6ec83c79c38239eb43112225db17731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 31 Jan 2023 15:14:16 +0100 Subject: [PATCH 192/906] =?UTF-8?q?=F0=9F=94=A7=20Update=20new=20issue=20c?= =?UTF-8?q?hooser=20to=20point=20to=20GitHub=20Discussions=20(#546)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/config.yml | 9 + .github/ISSUE_TEMPLATE/feature-request.yml | 214 --------------------- .github/ISSUE_TEMPLATE/privileged.yml | 22 +++ .github/ISSUE_TEMPLATE/question.yml | 162 ---------------- 4 files changed, 31 insertions(+), 376 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/feature-request.yml create mode 100644 .github/ISSUE_TEMPLATE/privileged.yml delete mode 100644 .github/ISSUE_TEMPLATE/question.yml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 55749398fd..46a5c2c216 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,3 +2,12 @@ blank_issues_enabled: false contact_links: - name: Security Contact about: Please report security vulnerabilities to security@tiangolo.com + - name: Question or Problem + about: Ask a question or ask about a problem in GitHub Discussions. + url: https://github.com/tiangolo/sqlmodel/discussions/categories/questions + - name: Feature Request + about: To suggest an idea or ask about a feature, please start with a question saying what you would like to achieve. There might be a way to do it already. + url: https://github.com/tiangolo/sqlmodel/discussions/categories/questions + - name: Show and tell + about: Show what you built with SQLModel or to be used with SQLModel. + url: https://github.com/tiangolo/sqlmodel/discussions/categories/show-and-tell diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml deleted file mode 100644 index 5f18e8d420..0000000000 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ /dev/null @@ -1,214 +0,0 @@ -name: Feature Request -description: Suggest an idea or ask for a feature that you would like to have in SQLModel -labels: [enhancement] -body: - - type: markdown - attributes: - value: | - Thanks for your interest in SQLModel! 🚀 - - Please follow these instructions, fill every question, and do every step. 🙏 - - I'm asking this because answering questions and solving problems in GitHub issues is what consumes most of the time. - - I end up not being able to add new features, fix bugs, review pull requests, etc. as fast as I wish because I have to spend too much time handling issues. - - All that, on top of all the incredible help provided by a bunch of community members that give a lot of their time to come here and help others. - - If more SQLModel users came to help others like them just a little bit more, it would be much less effort for them (and you and me 😅). - - By asking questions in a structured way (following this) it will be much easier to help you. - - And there's a high chance that you will find the solution along the way and you won't even have to submit it and wait for an answer. 😎 - - As there are too many issues with questions, I'll have to close the incomplete ones. That will allow me (and others) to focus on helping people like you that follow the whole process and help us help you. 🤓 - - type: checkboxes - id: checks - attributes: - label: First Check - description: Please confirm and check all the following options. - options: - - label: I added a very descriptive title to this issue. - required: true - - label: I used the GitHub search to find a similar issue and didn't find it. - required: true - - label: I searched the SQLModel documentation, with the integrated search. - required: true - - label: I already searched in Google "How to X in SQLModel" and didn't find any information. - required: true - - label: I already read and followed all the tutorial in the docs and didn't find an answer. - required: true - - label: I already checked if it is not related to SQLModel but to [Pydantic](https://github.com/samuelcolvin/pydantic). - required: true - - label: I already checked if it is not related to SQLModel but to [SQLAlchemy](https://github.com/sqlalchemy/sqlalchemy). - required: true - - type: checkboxes - id: help - attributes: - label: Commit to Help - description: | - After submitting this, I commit to one of: - - * Read open issues with questions until I find 2 issues where I can help someone and add a comment to help there. - * I already hit the "watch" button in this repository to receive notifications and I commit to help at least 2 people that ask questions in the future. - * Implement a Pull Request for a confirmed bug. - - options: - - label: I commit to help with one of those options 👆 - required: true - - type: textarea - id: example - attributes: - label: Example Code - description: | - Please add a self-contained, [minimal, reproducible, example](https://stackoverflow.com/help/minimal-reproducible-example) with your use case. - - If I (or someone) can copy it, run it, and see it right away, there's a much higher chance I (or someone) will be able to help you. - - placeholder: | - from typing import Optional - - from sqlmodel import Field, Session, SQLModel, create_engine - - - class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str - secret_name: str - age: Optional[int] = None - - - hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") - - engine = create_engine("sqlite:///database.db") - - - SQLModel.metadata.create_all(engine) - - with Session(engine) as session: - session.add(hero_1) - session.commit() - session.refresh(hero_1) - print(hero_1) - render: python - validations: - required: true - - type: textarea - id: description - attributes: - label: Description - description: | - What is your feature request? - - Write a short description telling me what you are trying to solve and what you are currently doing. - placeholder: | - * Create a Hero model. - * Create a Hero instance. - * Save it to a SQLite database. - * I would like it to also automatically send me an email with all the SQL code executed. - validations: - required: true - - type: textarea - id: wanted-solution - attributes: - label: Wanted Solution - description: | - Tell me what's the solution you would like. - placeholder: | - I would like it to have a `send_email` configuration that defaults to `False`, and can be set to `True` to send me an email. - validations: - required: true - - type: textarea - id: wanted-code - attributes: - label: Wanted Code - description: Show me an example of how you would want the code to look like. - placeholder: | - from typing import Optional - - from sqlmodel import Field, Session, SQLModel, create_engine - - - class Hero(SQLModel, table=True, send_email=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str - secret_name: str - age: Optional[int] = None - - - hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") - - engine = create_engine("sqlite:///database.db") - - - SQLModel.metadata.create_all(engine) - - with Session(engine) as session: - session.add(hero_1) - session.commit() - session.refresh(hero_1) - print(hero_1) - - - render: python - validations: - required: true - - type: textarea - id: alternatives - attributes: - label: Alternatives - description: | - Tell me about alternatives you've considered. - placeholder: | - To hire someone to look at the logs, write the SQL in paper, and then send me a letter by post with it. - - type: dropdown - id: os - attributes: - label: Operating System - description: What operating system are you on? - multiple: true - options: - - Linux - - Windows - - macOS - - Other - validations: - required: true - - type: textarea - id: os-details - attributes: - label: Operating System Details - description: You can add more details about your operating system here, in particular if you chose "Other". - - type: input - id: sqlmodel-version - attributes: - label: SQLModel Version - description: | - What SQLModel version are you using? - - You can find the SQLModel version with: - - ```bash - python -c "import sqlmodel; print(sqlmodel.__version__)" - ``` - validations: - required: true - - type: input - id: python-version - attributes: - label: Python Version - description: | - What Python version are you using? - - You can find the Python version with: - - ```bash - python --version - ``` - validations: - required: true - - type: textarea - id: context - attributes: - label: Additional Context - description: Add any additional context information or screenshots you think are useful. diff --git a/.github/ISSUE_TEMPLATE/privileged.yml b/.github/ISSUE_TEMPLATE/privileged.yml new file mode 100644 index 0000000000..da11e718eb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/privileged.yml @@ -0,0 +1,22 @@ +name: Privileged +description: You are @tiangolo or he asked you directly to create an issue here. If not, check the other options. 👇 +body: + - type: markdown + attributes: + value: | + Thanks for your interest in SQLModel! 🚀 + + If you are not @tiangolo or he didn't ask you directly to create an issue here, please start the conversation in a [Question in GitHub Discussions](https://github.com/tiangolo/sqlmodel/discussions/categories/questions) instead. + - type: checkboxes + id: privileged + attributes: + label: Privileged issue + description: Confirm that you are allowed to create an issue here. + options: + - label: I'm @tiangolo or he asked me directly to create an issue here. + required: true + - type: textarea + id: content + attributes: + label: Issue Content + description: Add the content of the issue here. diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml deleted file mode 100644 index dfdc25dc89..0000000000 --- a/.github/ISSUE_TEMPLATE/question.yml +++ /dev/null @@ -1,162 +0,0 @@ -name: Question or Problem -description: Ask a question or ask about a problem -labels: [question] -body: - - type: markdown - attributes: - value: | - Thanks for your interest in SQLModel! 🚀 - - Please follow these instructions, fill every question, and do every step. 🙏 - - I'm asking this because answering questions and solving problems in GitHub is what consumes most of the time. - - I end up not being able to add new features, fix bugs, review pull requests, etc. as fast as I wish because I have to spend too much time handling questions. - - All that, on top of all the incredible help provided by a bunch of community members that give a lot of their time to come here and help others. - - If more SQLModel users came to help others like them just a little bit more, it would be much less effort for them (and you and me 😅). - - By asking questions in a structured way (following this) it will be much easier to help you. - - And there's a high chance that you will find the solution along the way and you won't even have to submit it and wait for an answer. 😎 - - As there are too many questions, I'll have to discard and close the incomplete ones. That will allow me (and others) to focus on helping people like you that follow the whole process and help us help you. 🤓 - - type: checkboxes - id: checks - attributes: - label: First Check - description: Please confirm and check all the following options. - options: - - label: I added a very descriptive title here. - required: true - - label: I used the GitHub search to find a similar question and didn't find it. - required: true - - label: I searched the SQLModel documentation, with the integrated search. - required: true - - label: I already searched in Google "How to X in SQLModel" and didn't find any information. - required: true - - label: I already read and followed all the tutorial in the docs and didn't find an answer. - required: true - - label: I already checked if it is not related to SQLModel but to [Pydantic](https://github.com/samuelcolvin/pydantic). - required: true - - label: I already checked if it is not related to SQLModel but to [SQLAlchemy](https://github.com/sqlalchemy/sqlalchemy). - required: true - - type: checkboxes - id: help - attributes: - label: Commit to Help - description: | - After submitting this, I commit to one of: - - * Read open issues with questions until I find 2 issues where I can help someone and add a comment to help there. - * I already hit the "watch" button in this repository to receive notifications and I commit to help at least 2 people that ask questions in the future. - * Review one Pull Request by downloading the code and following all the review process](https://sqlmodel.tiangolo.com/help/#review-pull-requests). - - options: - - label: I commit to help with one of those options 👆 - required: true - - type: textarea - id: example - attributes: - label: Example Code - description: | - Please add a self-contained, [minimal, reproducible, example](https://stackoverflow.com/help/minimal-reproducible-example) with your use case. - - If I (or someone) can copy it, run it, and see it right away, there's a much higher chance I (or someone) will be able to help you. - - placeholder: | - from typing import Optional - - from sqlmodel import Field, Session, SQLModel, create_engine - - - class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str - secret_name: str - age: Optional[int] = None - - - hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") - - engine = create_engine("sqlite:///database.db") - - - SQLModel.metadata.create_all(engine) - - with Session(engine) as session: - session.add(hero_1) - session.commit() - session.refresh(hero_1) - print(hero_1) - render: python - validations: - required: true - - type: textarea - id: description - attributes: - label: Description - description: | - What is the problem, question, or error? - - Write a short description telling me what you are doing, what you expect to happen, and what is currently happening. - placeholder: | - * Create a Hero model. - * Create a Hero instance. - * Save it to a SQLite database. - * Check the data with DB Browser for SQLite. - * There's only one hero there, but I expected it to include many others recruited by the first one. - validations: - required: true - - type: dropdown - id: os - attributes: - label: Operating System - description: What operating system are you on? - multiple: true - options: - - Linux - - Windows - - macOS - - Other - validations: - required: true - - type: textarea - id: os-details - attributes: - label: Operating System Details - description: You can add more details about your operating system here, in particular if you chose "Other". - - type: input - id: sqlmodel-version - attributes: - label: SQLModel Version - description: | - What SQLModel version are you using? - - You can find the SQLModel version with: - - ```bash - python -c "import sqlmodel; print(sqlmodel.__version__)" - ``` - validations: - required: true - - type: input - id: python-version - attributes: - label: Python Version - description: | - What Python version are you using? - - You can find the Python version with: - - ```bash - python --version - ``` - validations: - required: true - - type: textarea - id: context - attributes: - label: Additional Context - description: Add any additional context information or screenshots you think are useful. From dc44d2e2c43d302bd9dbc9248365ec81efc498d0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 31 Jan 2023 14:14:55 +0000 Subject: [PATCH 193/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 9a680bcd07..7b59117dfb 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update new issue chooser to point to GitHub Discussions. PR [#546](https://github.com/tiangolo/sqlmodel/pull/546) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add template for GitHub Discussion questions and update issues template. PR [#544](https://github.com/tiangolo/sqlmodel/pull/544) by [@tiangolo](https://github.com/tiangolo). * 👷 Refactor CI artifact upload/download for docs previews. PR [#514](https://github.com/tiangolo/sqlmodel/pull/514) by [@tiangolo](https://github.com/tiangolo). * ✏️ Fix typo in internal function name `get_sqlachemy_type()`. PR [#496](https://github.com/tiangolo/sqlmodel/pull/496) by [@cmarqu](https://github.com/cmarqu). From 5c9a3b3b2181ee274be1cdf424ff9da26e377cbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 3 Feb 2023 18:52:25 +0100 Subject: [PATCH 194/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20help=20SQLModel?= =?UTF-8?q?=20docs=20(#548)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/help.md | 150 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 143 insertions(+), 7 deletions(-) diff --git a/docs/help.md b/docs/help.md index 6cde4c6142..d0d2308de2 100644 --- a/docs/help.md +++ b/docs/help.md @@ -58,26 +58,125 @@ You can: I love to hear about how **SQLModel** is being used, what you have liked in it, in which project/company are you using it, etc. -## Help others with issues in GitHub +## Help others with questions in GitHub -You can see existing issues and try and help others, most of the times they are questions that you might already know the answer for. 🤓 +You can try and help others with their questions in: + +* GitHub Discussions +* GitHub Issues + +In many cases you might already know the answer for those questions. 🤓 + +Just remember, the most important point is: try to be kind. People come with their frustrations and in many cases don't ask in the best way, but try as best as you can to be kind. 🤗 + +The idea is for the **SQLModel** community to be kind and welcoming. At the same time, don't accept bullying or disrespectful behavior towards others. We have to take care of each other. + +--- + +Here's how to help others with questions (in discussions or issues): + +### Understand the question + +* Check if you can understand what is the **purpose** and use case of the person asking. + +* Then check if the question (the vast majority are questions) is **clear**. + +* In many cases the question asked is about an imaginary solution from the user, but there might be a **better** one. If you can understand the problem and use case better, you might be able to suggest a better **alternative solution**. + +* If you can't understand the question, ask for more **details**. + +### Reproduce the problem + +For most of the cases and most of the questions there's something related to the person's **original code**. + +In many cases they will only copy a fragment of the code, but that's not enough to **reproduce the problem**. + +* You can ask them to provide a minimal, reproducible, example, that you can **copy-paste** and run locally to see the same error or behavior they are seeing, or to understand their use case better. + +* If you are feeling too generous, you can try to **create an example** like that yourself, just based on the description of the problem. Just have in mind that this might take a lot of time and it might be better to ask them to clarify the problem first. + +### Suggest solutions + +* After being able to understand the question, you can give them a possible **answer**. + +* In many cases, it's better to understand their **underlying problem or use case**, because there might be a better way to solve it than what they are trying to do. + +### Ask to close + +If they reply, there's a high chance you would have solved their problem, congrats, **you're a hero**! 🦸 + +* Now, if that solved their problem, you can ask them to: + + * In GitHub Discussions: mark the comment as the **answer**. + * In GitHub Issues: **close** the issue**. ## Watch the GitHub repository You can "watch" SQLModel in GitHub (clicking the "watch" button at the top right): https://github.com/tiangolo/sqlmodel. 👀 -If you select "Watching" instead of "Releases only" you will receive notifications when someone creates a new issue. +If you select "Watching" instead of "Releases only" you will receive notifications when someone creates a new issue or question. You can also specify that you only want to be notified about new issues, or discussions, or PRs, etc. -Then you can try and help them solve those issues. +Then you can try and help them solve those questions. -## Create issues +## Ask Questions -You can create a new issue in the GitHub repository, for example to: +You can create a new question in the GitHub repository, for example to: * Ask a **question** or ask about a **problem**. * Suggest a new **feature**. -**Note**: if you create an issue, then I'm going to ask you to also help others. 😉 +**Note**: if you do it, then I'm going to ask you to also help others. 😉 + +## Review Pull Requests + +You can help me review pull requests from others. + +Again, please try your best to be kind. 🤗 + +--- + +Here's what to have in mind and how to review a pull request: + +### Understand the problem + +* First, make sure you **understand the problem** that the pull request is trying to solve. It might have a longer discussion in a GitHub Discussion or issue. + +* There's also a good chance that the pull request is not actually needed because the problem can be solved in a **different way**. Then you can suggest or ask about that. + +### Don't worry about style + +* Don't worry too much about things like commit message styles, I will squash and merge customizing the commit manually. + +* Also don't worry about style rules, there are already automatized tools checking that. + +And if there's any other style or consistency need, I'll ask directly for that, or I'll add commits on top with the needed changes. + +### Check the code + +* Check and read the code, see if it makes sense, **run it locally** and see if it actually solves the problem. + +* Then **comment** saying that you did that, that's how I will know you really checked it. + +!!! info + Unfortunately, I can't simply trust PRs that just have several approvals. + + Several times it has happened that there are PRs with 3, 5 or more approvals, probably because the description is appealing, but when I check the PRs, they are actually broken, have a bug, or don't solve the problem they claim to solve. 😅 + + So, it's really important that you actually read and run the code, and let me know in the comments that you did. 🤓 + +* If the PR can be simplified in a way, you can ask for that, but there's no need to be too picky, there might be a lot of subjective points of view (and I will have my own as well 🙈), so it's better if you can focus on the fundamental things. + +### Tests + +* Help me check that the PR has **tests**. + +* Check that the tests **fail** before the PR. 🚨 + +* Then check that the tests **pass** after the PR. ✅ + +* Many PRs don't have tests, you can **remind** them to add tests, or you can even **suggest** some tests yourself. That's one of the things that consume most time and you can help a lot with that. + +* Then also comment what you tried, that way I'll know that you checked it. 🤓 ## Create a Pull Request @@ -86,7 +185,44 @@ You can [contribute](contributing.md){.internal-link target=_blank} to the sourc * To fix a typo you found on the documentation. * To propose new documentation sections. * To fix an existing issue/bug. + * Make sure to add tests. * To add a new feature. + * Make sure to add tests. + * Make sure to add documentation if it's relevant. + +## Help Maintain SQLModel + +Help me maintain **SQLModel**! 🤓 + +There's a lot of work to do, and for most of it, **YOU** can do it. + +The main tasks that you can do right now are: + +* [Help others with questions in GitHub](#help-others-with-questions-in-github){.internal-link target=_blank} (see the section above). +* [Review Pull Requests](#review-pull-requests){.internal-link target=_blank} (see the section above). + +Those two tasks are what **consume time the most**. That's the main work of maintaining SQLModel. + +If you can help me with that, **you are helping me maintain SQLModel** and making sure it keeps **advancing faster and better**. 🚀 + +## Join the chat + +Join the 👥 FastAPI and Friends Discord chat server 👥 and hang out with others in the community. There's a `#sqlmodel` channel. + +!!! tip + For questions, ask them in GitHub Discussions, there's a much better chance you will receive help there. + + Use the chat only for other general conversations. + +### Don't use the chat for questions + +Have in mind that as chats allow more "free conversation", it's easy to ask questions that are too general and more difficult to answer, so, you might not receive answers. + +In GitHub, the template will guide you to write the right question so that you can more easily get a good answer, or even solve the problem yourself even before asking. And in GitHub I can make sure I always answer everything, even if it takes some time. I can't personally do that with the chat. 😅 + +Conversations in the chat are also not as easily searchable as in GitHub, so questions and answers might get lost in the conversation. + +On the other side, there are thousands of users in the chat, so there's a high chance you'll find someone to talk to there, almost all the time. 😄 ## Sponsor the author From 33e00c3ab3ed2143c583f94ed0b6bfdde0d72c07 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 3 Feb 2023 17:53:01 +0000 Subject: [PATCH 195/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 7b59117dfb..9f8ce2abab 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Update help SQLModel docs. PR [#548](https://github.com/tiangolo/sqlmodel/pull/548) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update new issue chooser to point to GitHub Discussions. PR [#546](https://github.com/tiangolo/sqlmodel/pull/546) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add template for GitHub Discussion questions and update issues template. PR [#544](https://github.com/tiangolo/sqlmodel/pull/544) by [@tiangolo](https://github.com/tiangolo). * 👷 Refactor CI artifact upload/download for docs previews. PR [#514](https://github.com/tiangolo/sqlmodel/pull/514) by [@tiangolo](https://github.com/tiangolo). From 810236c26c6d46a2d2b008e1640a3f41f0c6a77e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 21 Feb 2023 12:02:18 +0100 Subject: [PATCH 196/906] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20analytic?= =?UTF-8?q?s=20(#558)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index a27bbde8a1..646af7c39e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -110,7 +110,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-205713594-2 + property: G-J8HVTT936W social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/sqlmodel From 43a689d369f52b72aac60efd71111aba7d84714d Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 21 Feb 2023 11:02:54 +0000 Subject: [PATCH 197/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 9f8ce2abab..0ea7801b6e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆️ Upgrade analytics. PR [#558](https://github.com/tiangolo/sqlmodel/pull/558) by [@tiangolo](https://github.com/tiangolo). * 📝 Update help SQLModel docs. PR [#548](https://github.com/tiangolo/sqlmodel/pull/548) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update new issue chooser to point to GitHub Discussions. PR [#546](https://github.com/tiangolo/sqlmodel/pull/546) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add template for GitHub Discussion questions and update issues template. PR [#544](https://github.com/tiangolo/sqlmodel/pull/544) by [@tiangolo](https://github.com/tiangolo). From 02bd7ebffdeda14110cff3f1bb7472a77a17f73b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 29 Jul 2023 12:32:47 +0200 Subject: [PATCH 198/906] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Deprecate=20Pyt?= =?UTF-8?q?hon=203.6=20and=20upgrade=20Poetry=20and=20Poetry=20Version=20P?= =?UTF-8?q?lugin=20(#627)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 32 +++++++++++------------ .github/workflows/publish.yml | 16 +++++------- .github/workflows/test.yml | 42 ++++++++++++++++++++----------- README.md | 2 +- docs/contributing.md | 4 --- docs/features.md | 2 +- docs/index.md | 2 +- docs/tutorial/index.md | 6 +---- pyproject.toml | 12 ++++----- scripts/lint.sh | 2 -- scripts/test.sh | 1 + sqlmodel/main.py | 3 ++- sqlmodel/sql/expression.py | 37 ++++++--------------------- sqlmodel/sql/expression.py.jinja2 | 40 ++++++----------------------- 14 files changed, 77 insertions(+), 124 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 6400691533..3bcc78dfa5 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -4,26 +4,28 @@ on: branches: - main pull_request: - types: [opened, synchronize] + types: + - opened + - synchronize workflow_dispatch: inputs: debug_enabled: - description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' + description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' required: false default: false jobs: build-docs: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - name: Dump GitHub context env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v3.1.0 + - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.7" + python-version: "3.11" # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 @@ -34,34 +36,30 @@ jobs: id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-root-docs + key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-root-docs-v2 - name: Install poetry if: steps.cache.outputs.cache-hit != 'true' - # TODO: remove python -m pip install --force git+https://github.com/python-poetry/poetry-core.git@ad33bc2 - # once there's a release of Poetry 1.2.x including poetry-core > 1.1.0a6 - # Ref: https://github.com/python-poetry/poetry-core/pull/188 run: | python -m pip install --upgrade pip - python -m pip install --force git+https://github.com/python-poetry/poetry-core.git@ad33bc2 - python -m pip install "poetry==1.2.0a2" - python -m poetry plugin add poetry-version-plugin + python -m pip install "poetry" + python -m poetry self add poetry-version-plugin - name: Configure poetry run: python -m poetry config virtualenvs.create false - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: python -m poetry install - name: Install Material for MkDocs Insiders - if: github.event.pull_request.head.repo.fork == false && steps.cache.outputs.cache-hit != 'true' - run: python -m poetry run pip install git+https://${{ secrets.ACTIONS_TOKEN }}@github.com/squidfunk/mkdocs-material-insiders.git + if: ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false ) && steps.cache.outputs.cache-hit != 'true' + run: python -m poetry run pip install git+https://${{ secrets.SQLMODEL_MKDOCS_MATERIAL_INSIDERS }}@github.com/squidfunk/mkdocs-material-insiders.git - uses: actions/cache@v3 with: key: mkdocs-cards-${{ github.ref }} path: .cache - name: Build Docs - if: github.event.pull_request.head.repo.fork == true + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true run: python -m poetry run mkdocs build - name: Build Docs with Insiders - if: github.event.pull_request.head.repo.fork == false + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false run: python -m poetry run mkdocs build --config-file mkdocs.insiders.yml - name: Zip docs run: python -m poetry run bash ./scripts/zip-docs.sh @@ -70,7 +68,7 @@ jobs: name: docs-zip path: ./site/docs.zip - name: Deploy to Netlify - uses: nwtgck/actions-netlify@v1.1.5 + uses: nwtgck/actions-netlify@v2.0.0 with: publish-dir: './site' production-branch: main diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f3c1e980a6..d7884111cb 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -7,15 +7,15 @@ on: workflow_dispatch: inputs: debug_enabled: - description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' + description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' required: false default: false jobs: publish: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3.1.0 + - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: @@ -30,17 +30,13 @@ jobs: id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-root + key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-root-v2 - name: Install poetry if: steps.cache.outputs.cache-hit != 'true' - # TODO: remove python -m pip install --force git+https://github.com/python-poetry/poetry-core.git@ad33bc2 - # once there's a release of Poetry 1.2.x including poetry-core > 1.1.0a6 - # Ref: https://github.com/python-poetry/poetry-core/pull/188 run: | python -m pip install --upgrade pip - python -m pip install --force git+https://github.com/python-poetry/poetry-core.git@ad33bc2 - python -m pip install "poetry==1.2.0a2" - python -m poetry plugin add poetry-version-plugin + python -m pip install "poetry" + python -m poetry self add poetry-version-plugin - name: Configure poetry run: python -m poetry config virtualenvs.create false - name: Install Dependencies diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 585ffc0455..c7435325a7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,24 +5,30 @@ on: branches: - main pull_request: - types: [opened, synchronize] + types: + - opened + - synchronize workflow_dispatch: inputs: debug_enabled: - description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' + description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' required: false default: false jobs: test: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.6.15", "3.7", "3.8", "3.9", "3.10"] + python-version: + - "3.7" + - "3.8" + - "3.9" + - "3.10" fail-fast: false steps: - - uses: actions/checkout@v3.1.0 + - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: @@ -37,24 +43,19 @@ jobs: id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-root + key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-root-v2 - name: Install poetry if: steps.cache.outputs.cache-hit != 'true' - # TODO: remove python -m pip install --force git+https://github.com/python-poetry/poetry-core.git@ad33bc2 - # once there's a release of Poetry 1.2.x including poetry-core > 1.1.0a6 - # Ref: https://github.com/python-poetry/poetry-core/pull/188 run: | python -m pip install --upgrade pip - python -m pip install --force git+https://github.com/python-poetry/poetry-core.git@ad33bc2 - python -m pip install "poetry==1.2.0a2" - python -m poetry plugin add poetry-version-plugin + python -m pip install "poetry" + python -m poetry self add poetry-version-plugin - name: Configure poetry run: python -m poetry config virtualenvs.create false - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: python -m poetry install - name: Lint - if: ${{ matrix.python-version != '3.6.15' }} run: python -m poetry run bash scripts/lint.sh - run: mkdir coverage - name: Test @@ -68,7 +69,8 @@ jobs: name: coverage path: coverage coverage-combine: - needs: [test] + needs: + - test runs-on: ubuntu-latest steps: @@ -96,3 +98,15 @@ jobs: with: name: coverage-html path: htmlcov + + # https://github.com/marketplace/actions/alls-green#why + alls-green: # This job does nothing and is only used for the branch protection + if: always() + needs: + - coverage-combine + runs-on: ubuntu-latest + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} diff --git a/README.md b/README.md index 5721f1cdb0..f85bb97a6d 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ It combines SQLAlchemy and Pydantic and tries to simplify the code you write as ## Requirements -A recent and currently supported version of Python (right now, Python supports versions 3.6 and above). +A recent and currently supported version of Python Python. As **SQLModel** is based on **Pydantic** and **SQLAlchemy**, it requires them. They will be automatically installed when you install SQLModel. diff --git a/docs/contributing.md b/docs/contributing.md index f2964fba9b..1cd62d42dc 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -6,10 +6,6 @@ First, you might want to see the basic ways to [help SQLModel and get help](help If you already cloned the repository and you know that you need to deep dive in the code, here are some guidelines to set up your environment. -### Python - -SQLModel supports Python 3.6 and above, but for development you should have at least **Python 3.7**. - ### Poetry **SQLModel** uses Poetry to build, package, and publish the project. diff --git a/docs/features.md b/docs/features.md index 09de0c17f9..102edef725 100644 --- a/docs/features.md +++ b/docs/features.md @@ -12,7 +12,7 @@ Nevertheless, SQLModel is completely **independent** of FastAPI and can be used ## Just Modern Python -It's all based on standard modern **Python** type annotations. No new syntax to learn. Just standard modern Python. +It's all based on standard modern **Python** type annotations. No new syntax to learn. Just standard modern Python. If you need a 2 minute refresher of how to use Python types (even if you don't use SQLModel or FastAPI), check the FastAPI tutorial section: Python types intro. diff --git a/docs/index.md b/docs/index.md index 5721f1cdb0..f85bb97a6d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -50,7 +50,7 @@ It combines SQLAlchemy and Pydantic and tries to simplify the code you write as ## Requirements -A recent and currently supported version of Python (right now, Python supports versions 3.6 and above). +A recent and currently supported version of Python Python. As **SQLModel** is based on **Pydantic** and **SQLAlchemy**, it requires them. They will be automatically installed when you install SQLModel. diff --git a/docs/tutorial/index.md b/docs/tutorial/index.md index 33cf6226c4..79fa670cfd 100644 --- a/docs/tutorial/index.md +++ b/docs/tutorial/index.md @@ -64,15 +64,13 @@ $ cd sqlmodel-tutorial Make sure you have an officially supported version of Python. -Currently it is **Python 3.6** and above (Python 3.5 was already deprecated). - You can check which version you have with:
```console $ python3 --version -Python 3.6.9 +Python 3.11 ```
@@ -84,8 +82,6 @@ You might want to try with the specific versions, for example with: * `python3.10` * `python3.9` * `python3.8` -* `python3.7` -* `python3.6` The code would look like this: diff --git a/pyproject.toml b/pyproject.toml index e3b1d3c279..e402727150 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,10 +17,10 @@ classifiers = [ "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", "Topic :: Database", "Topic :: Database :: Database Engines/Servers", "Topic :: Internet", @@ -30,7 +30,7 @@ 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} @@ -39,19 +39,17 @@ sqlalchemy2-stubs = {version = "*", allow-prereleases = true} pytest = "^7.0.1" mypy = "0.971" flake8 = "^5.0.4" -black = {version = "^22.10.0", python = "^3.7"} +black = "^22.10.0" mkdocs = "^1.2.1" mkdocs-material = "^8.1.4" -pillow = {version = "^9.3.0", python = "^3.7"} -cairosvg = {version = "^2.5.2", python = "^3.7"} +pillow = "^9.3.0" +cairosvg = "^2.5.2" mdx-include = "^1.4.1" coverage = {extras = ["toml"], version = "^6.2"} fastapi = "^0.68.1" requests = "^2.26.0" autoflake = "^1.4" isort = "^5.9.3" -async_generator = {version = "*", python = "~3.6"} -async-exit-stack = {version = "*", python = "~3.6"} [build-system] requires = ["poetry-core"] diff --git a/scripts/lint.sh b/scripts/lint.sh index 02568cda6b..4191d90f1f 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -7,5 +7,3 @@ mypy sqlmodel flake8 sqlmodel tests docs_src black sqlmodel tests docs_src --check isort sqlmodel tests docs_src scripts --check-only -# TODO: move this to test.sh after deprecating Python 3.6 -CHECK_JINJA=1 python scripts/generate_select.py diff --git a/scripts/test.sh b/scripts/test.sh index 9b758bdbdf..1460a9c7ec 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -3,6 +3,7 @@ set -e set -x +CHECK_JINJA=1 python scripts/generate_select.py coverage run -m pytest tests coverage combine coverage report --show-missing diff --git a/sqlmodel/main.py b/sqlmodel/main.py index d95c498507..5b5950a811 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/sqlmodel/sql/expression.py b/sqlmodel/sql/expression.py index 31c0bc1a1e..264e39cba7 100644 --- a/sqlmodel/sql/expression.py +++ b/sqlmodel/sql/expression.py @@ -1,6 +1,5 @@ # WARNING: do not modify this code, it is generated by expression.py.jinja2 -import sys from datetime import datetime from typing import ( TYPE_CHECKING, @@ -12,7 +11,6 @@ Type, TypeVar, Union, - cast, overload, ) from uuid import UUID @@ -24,36 +22,17 @@ _TSelect = TypeVar("_TSelect") -# Workaround Generics incompatibility in Python 3.6 -# Ref: https://github.com/python/typing/issues/449#issuecomment-316061322 -if sys.version_info.minor >= 7: - class Select(_Select, Generic[_TSelect]): - inherit_cache = True +class Select(_Select, Generic[_TSelect]): + inherit_cache = True - # This is not comparable to sqlalchemy.sql.selectable.ScalarSelect, that has a different - # purpose. This is the same as a normal SQLAlchemy Select class where there's only one - # entity, so the result will be converted to a scalar by default. This way writing - # for loops on the results will feel natural. - class SelectOfScalar(_Select, Generic[_TSelect]): - inherit_cache = True -else: - from typing import GenericMeta # type: ignore - - class GenericSelectMeta(GenericMeta, _Select.__class__): # type: ignore - pass - - class _Py36Select(_Select, Generic[_TSelect], metaclass=GenericSelectMeta): - inherit_cache = True - - class _Py36SelectOfScalar(_Select, Generic[_TSelect], metaclass=GenericSelectMeta): - inherit_cache = True - - # Cast them for editors to work correctly, from several tricks tried, this works - # for both VS Code and PyCharm - Select = cast("Select", _Py36Select) # type: ignore - SelectOfScalar = cast("SelectOfScalar", _Py36SelectOfScalar) # type: ignore +# This is not comparable to sqlalchemy.sql.selectable.ScalarSelect, that has a different +# purpose. This is the same as a normal SQLAlchemy Select class where there's only one +# entity, so the result will be converted to a scalar by default. This way writing +# for loops on the results will feel natural. +class SelectOfScalar(_Select, Generic[_TSelect]): + inherit_cache = True if TYPE_CHECKING: # pragma: no cover diff --git a/sqlmodel/sql/expression.py.jinja2 b/sqlmodel/sql/expression.py.jinja2 index 51f04a215d..26d12a0395 100644 --- a/sqlmodel/sql/expression.py.jinja2 +++ b/sqlmodel/sql/expression.py.jinja2 @@ -1,4 +1,3 @@ -import sys from datetime import datetime from typing import ( TYPE_CHECKING, @@ -10,7 +9,6 @@ from typing import ( Type, TypeVar, Union, - cast, overload, ) from uuid import UUID @@ -22,37 +20,15 @@ from sqlalchemy.sql.expression import Select as _Select _TSelect = TypeVar("_TSelect") -# Workaround Generics incompatibility in Python 3.6 -# Ref: https://github.com/python/typing/issues/449#issuecomment-316061322 -if sys.version_info.minor >= 7: - - class Select(_Select, Generic[_TSelect]): - inherit_cache = True - - # This is not comparable to sqlalchemy.sql.selectable.ScalarSelect, that has a different - # purpose. This is the same as a normal SQLAlchemy Select class where there's only one - # entity, so the result will be converted to a scalar by default. This way writing - # for loops on the results will feel natural. - class SelectOfScalar(_Select, Generic[_TSelect]): - inherit_cache = True - -else: - from typing import GenericMeta # type: ignore - - class GenericSelectMeta(GenericMeta, _Select.__class__): # type: ignore - pass - - class _Py36Select(_Select, Generic[_TSelect], metaclass=GenericSelectMeta): - inherit_cache = True - - class _Py36SelectOfScalar(_Select, Generic[_TSelect], metaclass=GenericSelectMeta): - inherit_cache = True - - # Cast them for editors to work correctly, from several tricks tried, this works - # for both VS Code and PyCharm - Select = cast("Select", _Py36Select) # type: ignore - SelectOfScalar = cast("SelectOfScalar", _Py36SelectOfScalar) # type: ignore +class Select(_Select, Generic[_TSelect]): + inherit_cache = True +# This is not comparable to sqlalchemy.sql.selectable.ScalarSelect, that has a different +# purpose. This is the same as a normal SQLAlchemy Select class where there's only one +# entity, so the result will be converted to a scalar by default. This way writing +# for loops on the results will feel natural. +class SelectOfScalar(_Select, Generic[_TSelect]): + inherit_cache = True if TYPE_CHECKING: # pragma: no cover from ..main import SQLModel From a21d5c85a3b5131a2c9eb9fedcc6013796fa1063 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 31 Jul 2023 17:15:43 +0200 Subject: [PATCH 199/906] =?UTF-8?q?=F0=9F=91=B7=E2=80=8D=E2=99=82=EF=B8=8F?= =?UTF-8?q?=20Upgrade=20CI=20for=20docs=20(#628)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/actions/watch-previews/Dockerfile | 7 -- .github/actions/watch-previews/action.yml | 10 -- .github/actions/watch-previews/app/main.py | 102 ------------------ .github/workflows/build-docs.yml | 69 +++++++----- .../{preview-docs.yml => deploy-docs.yml} | 31 +++--- 5 files changed, 59 insertions(+), 160 deletions(-) delete mode 100644 .github/actions/watch-previews/Dockerfile delete mode 100644 .github/actions/watch-previews/action.yml delete mode 100644 .github/actions/watch-previews/app/main.py rename .github/workflows/{preview-docs.yml => deploy-docs.yml} (57%) diff --git a/.github/actions/watch-previews/Dockerfile b/.github/actions/watch-previews/Dockerfile deleted file mode 100644 index b8cc64d948..0000000000 --- a/.github/actions/watch-previews/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM python:3.7 - -RUN pip install httpx PyGithub "pydantic==1.5.1" - -COPY ./app /app - -CMD ["python", "/app/main.py"] diff --git a/.github/actions/watch-previews/action.yml b/.github/actions/watch-previews/action.yml deleted file mode 100644 index a9c4b3f78a..0000000000 --- a/.github/actions/watch-previews/action.yml +++ /dev/null @@ -1,10 +0,0 @@ -name: Watch docs previews in PRs -description: Check PRs and trigger new docs deploys -author: "Sebastián Ramírez " -inputs: - token: - description: 'Token for the repo. Can be passed in using {{ secrets.GITHUB_TOKEN }}' - required: true -runs: - using: docker - image: Dockerfile diff --git a/.github/actions/watch-previews/app/main.py b/.github/actions/watch-previews/app/main.py deleted file mode 100644 index 8a6d4a2525..0000000000 --- a/.github/actions/watch-previews/app/main.py +++ /dev/null @@ -1,102 +0,0 @@ -import logging -from datetime import datetime -from pathlib import Path -from typing import List, Optional - -import httpx -from github import Github -from github.NamedUser import NamedUser -from pydantic import BaseModel, BaseSettings, SecretStr - -github_api = "https://api.github.com" -netlify_api = "https://api.netlify.com" -main_branch = "main" - - -class Settings(BaseSettings): - input_token: SecretStr - github_repository: str - github_event_path: Path - github_event_name: Optional[str] = None - - -class Artifact(BaseModel): - id: int - node_id: str - name: str - size_in_bytes: int - url: str - archive_download_url: str - expired: bool - created_at: datetime - updated_at: datetime - - -class ArtifactResponse(BaseModel): - total_count: int - artifacts: List[Artifact] - - -def get_message(commit: str) -> str: - return f"Docs preview for commit {commit} at" - - -if __name__ == "__main__": - logging.basicConfig(level=logging.INFO) - settings = Settings() - logging.info(f"Using config: {settings.json()}") - g = Github(settings.input_token.get_secret_value()) - repo = g.get_repo(settings.github_repository) - owner: NamedUser = repo.owner - headers = {"Authorization": f"token {settings.input_token.get_secret_value()}"} - prs = list(repo.get_pulls(state="open")) - response = httpx.get( - f"{github_api}/repos/{settings.github_repository}/actions/artifacts", - headers=headers, - ) - data = response.json() - artifacts_response = ArtifactResponse.parse_obj(data) - for pr in prs: - logging.info("-----") - logging.info(f"Processing PR #{pr.number}: {pr.title}") - pr_comments = list(pr.get_issue_comments()) - pr_commits = list(pr.get_commits()) - last_commit = pr_commits[0] - for pr_commit in pr_commits: - if pr_commit.commit.author.date > last_commit.commit.author.date: - last_commit = pr_commit - commit = last_commit.commit.sha - logging.info(f"Last commit: {commit}") - message = get_message(commit) - notified = False - for pr_comment in pr_comments: - if message in pr_comment.body: - notified = True - logging.info(f"Docs preview was notified: {notified}") - if not notified: - artifact_name = f"docs-zip-{commit}" - use_artifact: Optional[Artifact] = None - for artifact in artifacts_response.artifacts: - if artifact.name == artifact_name: - use_artifact = artifact - break - if not use_artifact: - logging.info("Artifact not available") - else: - logging.info(f"Existing artifact: {use_artifact.name}") - response = httpx.post( - f"{github_api}/repos/{settings.github_repository}/actions/workflows/preview-docs.yml/dispatches", - headers=headers, - json={ - "ref": main_branch, - "inputs": { - "pr": f"{pr.number}", - "name": artifact_name, - "commit": commit, - }, - }, - ) - logging.info( - f"Trigger sent, response status: {response.status_code} - content: {response.content}" - ) - logging.info("Finished") diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 3bcc78dfa5..cf5295aae0 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -7,14 +7,34 @@ on: types: - opened - synchronize - workflow_dispatch: - inputs: - debug_enabled: - description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' - required: false - default: false jobs: + changes: + runs-on: ubuntu-latest + # Required permissions + permissions: + pull-requests: read + # Set job outputs to values from filter step + outputs: + docs: ${{ steps.filter.outputs.docs }} + steps: + - uses: actions/checkout@v3 + # For pull requests it's not necessary to checkout the code but for the main branch it is + - uses: dorny/paths-filter@v2 + id: filter + with: + filters: | + docs: + - README.md + - docs/** + - docs_src/** + - pyproject.toml + - mkdocs.yml + - mkdocs.insiders.yml + build-docs: + needs: + - changes + if: ${{ needs.changes.outputs.docs == 'true' }} runs-on: ubuntu-latest steps: - name: Dump GitHub context @@ -26,18 +46,12 @@ jobs: uses: actions/setup-python@v4 with: python-version: "3.11" - # Allow debugging with tmate - - name: Setup tmate session - uses: mxschmitt/action-tmate@v3 - if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }} - with: - limit-access-to-actor: true - uses: actions/cache@v3 id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-root-docs-v2 - - name: Install poetry + key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-v01 + - name: Install Poetry if: steps.cache.outputs.cache-hit != 'true' run: | python -m pip install --upgrade pip @@ -61,19 +75,20 @@ jobs: - name: Build Docs with Insiders if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false run: python -m poetry run mkdocs build --config-file mkdocs.insiders.yml - - name: Zip docs - run: python -m poetry run bash ./scripts/zip-docs.sh - uses: actions/upload-artifact@v3 with: - name: docs-zip - path: ./site/docs.zip - - name: Deploy to Netlify - uses: nwtgck/actions-netlify@v2.0.0 + name: docs-site + path: ./site/** + + # https://github.com/marketplace/actions/alls-green#why + docs-all-green: # This job does nothing and is only used for the branch protection + if: always() + needs: + - build-docs + runs-on: ubuntu-latest + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 with: - publish-dir: './site' - production-branch: main - github-token: ${{ secrets.GITHUB_TOKEN }} - enable-commit-comment: false - env: - NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} - NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} + jobs: ${{ toJSON(needs) }} + allowed-skips: build-docs diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/deploy-docs.yml similarity index 57% rename from .github/workflows/preview-docs.yml rename to .github/workflows/deploy-docs.yml index 3550a9b441..6fb0c01dcf 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -1,45 +1,48 @@ -name: Preview Docs +name: Deploy Docs on: workflow_run: workflows: - Build Docs - types: + types: - completed jobs: - preview-docs: - runs-on: ubuntu-20.04 + deploy-docs: + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3.1.0 + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@v3 - name: Clean site run: | rm -rf ./site mkdir ./site - name: Download Artifact Docs - uses: dawidd6/action-download-artifact@v2.24.2 + id: download + uses: dawidd6/action-download-artifact@v2.27.0 with: + if_no_artifact_found: ignore github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-docs.yml run_id: ${{ github.event.workflow_run.id }} - name: docs-zip + name: docs-site path: ./site/ - - name: Unzip docs - run: | - cd ./site - unzip docs.zip - rm -f docs.zip - name: Deploy to Netlify + if: steps.download.outputs.found_artifact == 'true' id: netlify - uses: nwtgck/actions-netlify@v1.1.5 + uses: nwtgck/actions-netlify@v2.0.0 with: publish-dir: './site' - production-deploy: false + production-deploy: ${{ github.event.workflow_run.head_repository.full_name == github.repository && github.event.workflow_run.head_branch == 'main' }} github-token: ${{ secrets.GITHUB_TOKEN }} enable-commit-comment: false env: NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} - name: Comment Deploy + if: steps.netlify.outputs.deploy-url != '' uses: ./.github/actions/comment-docs-preview-in-pr with: token: ${{ secrets.GITHUB_TOKEN }} From 73fa81af74a47e02acb2afacf93a714fa66d32e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 31 Jul 2023 21:38:43 +0200 Subject: [PATCH 200/906] =?UTF-8?q?=F0=9F=91=B7=20Update=20latest=20change?= =?UTF-8?q?s=20token=20(#616)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/latest-changes.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml index 9c3edccbf3..dd59e856d8 100644 --- a/.github/workflows/latest-changes.yml +++ b/.github/workflows/latest-changes.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@v3.1.0 with: # To allow latest-changes to commit to the main branch - token: ${{ secrets.ACTIONS_TOKEN }} + token: ${{ secrets.SQLMODEL_LATEST_CHANGES }} # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 From aaa54492c1646e0b0942e980db5a871b7ce0c420 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 31 Jul 2023 19:39:15 +0000 Subject: [PATCH 201/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 0ea7801b6e..fef2a4e926 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Update latest changes token. PR [#616](https://github.com/tiangolo/sqlmodel/pull/616) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade analytics. PR [#558](https://github.com/tiangolo/sqlmodel/pull/558) by [@tiangolo](https://github.com/tiangolo). * 📝 Update help SQLModel docs. PR [#548](https://github.com/tiangolo/sqlmodel/pull/548) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update new issue chooser to point to GitHub Discussions. PR [#546](https://github.com/tiangolo/sqlmodel/pull/546) by [@tiangolo](https://github.com/tiangolo). From e246ae88647ff096aff79f64e08cf9ef565c91d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 31 Jul 2023 21:48:21 +0200 Subject: [PATCH 202/906] =?UTF-8?q?=F0=9F=91=B7=20Update=20CI=20debug=20mo?= =?UTF-8?q?de=20with=20Tmate=20(#629)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/latest-changes.yml | 8 ++++---- .github/workflows/publish.yml | 4 ++-- .github/workflows/test.yml | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml index dd59e856d8..357767bba9 100644 --- a/.github/workflows/latest-changes.yml +++ b/.github/workflows/latest-changes.yml @@ -12,22 +12,22 @@ on: description: PR number required: true debug_enabled: - description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' + description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' required: false - default: false + default: 'false' jobs: latest-changes: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3.1.0 + - uses: actions/checkout@v3 with: # To allow latest-changes to commit to the main branch token: ${{ secrets.SQLMODEL_LATEST_CHANGES }} # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 - if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }} + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true - uses: docker://tiangolo/latest-changes:0.0.3 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d7884111cb..1c21cd4a87 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,7 +9,7 @@ on: debug_enabled: description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' required: false - default: false + default: 'false' jobs: publish: @@ -23,7 +23,7 @@ jobs: # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 - if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }} + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true - uses: actions/cache@v3 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c7435325a7..871557a6fe 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ on: debug_enabled: description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' required: false - default: false + default: 'false' jobs: test: @@ -36,7 +36,7 @@ jobs: # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 - if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }} + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true - uses: actions/cache@v3 From 40007c80da32edd9698f7bd063cc5d3496b60b60 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 31 Jul 2023 19:48:54 +0000 Subject: [PATCH 203/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index fef2a4e926..f7b4b08015 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Update CI debug mode with Tmate. PR [#629](https://github.com/tiangolo/sqlmodel/pull/629) by [@tiangolo](https://github.com/tiangolo). * 👷 Update latest changes token. PR [#616](https://github.com/tiangolo/sqlmodel/pull/616) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade analytics. PR [#558](https://github.com/tiangolo/sqlmodel/pull/558) by [@tiangolo](https://github.com/tiangolo). * 📝 Update help SQLModel docs. PR [#548](https://github.com/tiangolo/sqlmodel/pull/548) by [@tiangolo](https://github.com/tiangolo). From da29f7940d99f73eecb85cdc2caead7b163fdfe5 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 31 Jul 2023 19:52:51 +0000 Subject: [PATCH 204/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index f7b4b08015..d09e0baf6e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🗑️ Deprecate Python 3.6 and upgrade Poetry and Poetry Version Plugin. PR [#627](https://github.com/tiangolo/sqlmodel/pull/627) by [@tiangolo](https://github.com/tiangolo). * 👷 Update CI debug mode with Tmate. PR [#629](https://github.com/tiangolo/sqlmodel/pull/629) by [@tiangolo](https://github.com/tiangolo). * 👷 Update latest changes token. PR [#616](https://github.com/tiangolo/sqlmodel/pull/616) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade analytics. PR [#558](https://github.com/tiangolo/sqlmodel/pull/558) by [@tiangolo](https://github.com/tiangolo). From 30a9c23a348ede2f0b1c000f5e2b80517b2495cd Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 31 Jul 2023 19:53:00 +0000 Subject: [PATCH 205/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index d09e0baf6e..231e821073 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷‍♂️ Upgrade CI for docs. PR [#628](https://github.com/tiangolo/sqlmodel/pull/628) by [@tiangolo](https://github.com/tiangolo). * 🗑️ Deprecate Python 3.6 and upgrade Poetry and Poetry Version Plugin. PR [#627](https://github.com/tiangolo/sqlmodel/pull/627) by [@tiangolo](https://github.com/tiangolo). * 👷 Update CI debug mode with Tmate. PR [#629](https://github.com/tiangolo/sqlmodel/pull/629) by [@tiangolo](https://github.com/tiangolo). * 👷 Update latest changes token. PR [#616](https://github.com/tiangolo/sqlmodel/pull/616) by [@tiangolo](https://github.com/tiangolo). From 1e2fd100478943b7a8c8dda72f851ce39df18ee6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 1 Aug 2023 11:18:53 +0200 Subject: [PATCH 206/906] =?UTF-8?q?=F0=9F=91=B7=20Update=20docs=20deployme?= =?UTF-8?q?nts=20to=20Cloudflare=20(#630)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-docs.yml | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 6fb0c01dcf..25cd1ff369 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -29,21 +29,20 @@ jobs: run_id: ${{ github.event.workflow_run.id }} name: docs-site path: ./site/ - - name: Deploy to Netlify + - name: Deploy to Cloudflare Pages if: steps.download.outputs.found_artifact == 'true' - id: netlify - uses: nwtgck/actions-netlify@v2.0.0 + id: deploy + uses: cloudflare/pages-action@v1 with: - publish-dir: './site' - production-deploy: ${{ github.event.workflow_run.head_repository.full_name == github.repository && github.event.workflow_run.head_branch == 'main' }} - github-token: ${{ secrets.GITHUB_TOKEN }} - enable-commit-comment: false - env: - NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} - NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + projectName: sqlmodel + directory: './site' + gitHubToken: ${{ secrets.GITHUB_TOKEN }} + branch: ${{ ( github.event.workflow_run.head_repository.full_name == github.repository && github.event.workflow_run.head_branch == 'main' && 'main' ) || ( github.event.workflow_run.head_sha ) }} - name: Comment Deploy - if: steps.netlify.outputs.deploy-url != '' + if: steps.deploy.outputs.url != '' uses: ./.github/actions/comment-docs-preview-in-pr with: token: ${{ secrets.GITHUB_TOKEN }} - deploy_url: "${{ steps.netlify.outputs.deploy-url }}" + deploy_url: "${{ steps.deploy.outputs.url }}" From 088164ef2aa30a21a168f511a998f85c469bba1c Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 1 Aug 2023 09:19:41 +0000 Subject: [PATCH 207/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 231e821073..8b0cf528dd 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Update docs deployments to Cloudflare. PR [#630](https://github.com/tiangolo/sqlmodel/pull/630) by [@tiangolo](https://github.com/tiangolo). * 👷‍♂️ Upgrade CI for docs. PR [#628](https://github.com/tiangolo/sqlmodel/pull/628) by [@tiangolo](https://github.com/tiangolo). * 🗑️ Deprecate Python 3.6 and upgrade Poetry and Poetry Version Plugin. PR [#627](https://github.com/tiangolo/sqlmodel/pull/627) by [@tiangolo](https://github.com/tiangolo). * 👷 Update CI debug mode with Tmate. PR [#629](https://github.com/tiangolo/sqlmodel/pull/629) by [@tiangolo](https://github.com/tiangolo). From 89c356cb77b92728ed5962b17c9b19a17abc0af9 Mon Sep 17 00:00:00 2001 From: Sugato Ray Date: Sun, 22 Oct 2023 05:02:53 -0500 Subject: [PATCH 208/906] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20Add=20`CITATION?= =?UTF-8?q?.cff`=20file=20for=20academic=20citations=20(#13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- CITATION.cff | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 CITATION.cff diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000000..978031d58e --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,24 @@ +# This CITATION.cff file was generated with cffinit. +# Visit https://bit.ly/cffinit to generate yours today! + +cff-version: 1.2.0 +title: SQLModel +message: >- + If you use this software, please cite it using the + metadata from this file. +type: software +authors: + - given-names: Sebastián + family-names: Ramírez + email: tiangolo@gmail.com +identifiers: +repository-code: 'https://github.com/tiangolo/sqlmodel' +url: 'https://sqlmodel.tiangolo.com' +abstract: >- + SQLModel, SQL databases in Python, designed for + simplicity, compatibility, and robustness. +keywords: + - fastapi + - pydantic + - sqlalchemy +license: MIT From aa7169b93b2b06030da0a3e0ee86b4932be9415c Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 22 Oct 2023 10:03:26 +0000 Subject: [PATCH 209/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 8b0cf528dd..1f52793ea4 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🛠️ Add `CITATION.cff` file for academic citations. PR [#13](https://github.com/tiangolo/sqlmodel/pull/13) by [@sugatoray](https://github.com/sugatoray). * 👷 Update docs deployments to Cloudflare. PR [#630](https://github.com/tiangolo/sqlmodel/pull/630) by [@tiangolo](https://github.com/tiangolo). * 👷‍♂️ Upgrade CI for docs. PR [#628](https://github.com/tiangolo/sqlmodel/pull/628) by [@tiangolo](https://github.com/tiangolo). * 🗑️ Deprecate Python 3.6 and upgrade Poetry and Poetry Version Plugin. PR [#627](https://github.com/tiangolo/sqlmodel/pull/627) by [@tiangolo](https://github.com/tiangolo). From d5219aa3c55c5cb1ed2c0b1a665ef2ad10da425c Mon Sep 17 00:00:00 2001 From: byrman Date: Sun, 22 Oct 2023 14:01:51 +0200 Subject: [PATCH 210/906] =?UTF-8?q?=F0=9F=90=9B=20Fix=20SQLAlchemy=20versi?= =?UTF-8?q?on=201.4.36=20breaks=20SQLModel=20relationships=20(#315)=20(#46?= =?UTF-8?q?1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- sqlmodel/main.py | 1 + tests/test_main.py | 39 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 5b5950a811..caae8cf08d 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -333,6 +333,7 @@ def __init__( # There's a SQLAlchemy relationship declared, that takes precedence # over anything else, use that and continue with the next attribute dict_used[rel_name] = rel_info.sa_relationship + setattr(cls, rel_name, rel_info.sa_relationship) # Fix #315 continue ann = cls.__annotations__[rel_name] temp_field = ModelField.infer( diff --git a/tests/test_main.py b/tests/test_main.py index 22c62327da..72465cda33 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,8 +1,9 @@ -from typing import Optional +from typing import List, Optional import pytest from sqlalchemy.exc import IntegrityError -from sqlmodel import Field, Session, SQLModel, create_engine +from sqlalchemy.orm import RelationshipProperty +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine def test_should_allow_duplicate_row_if_unique_constraint_is_not_passed(clear_sqlmodel): @@ -91,3 +92,37 @@ class Hero(SQLModel, table=True): session.add(hero_2) session.commit() session.refresh(hero_2) + + +def test_sa_relationship_property(clear_sqlmodel): + """Test https://github.com/tiangolo/sqlmodel/issues/315#issuecomment-1272122306""" + + class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(unique=True) + heroes: List["Hero"] = Relationship( # noqa: F821 + sa_relationship=RelationshipProperty("Hero", back_populates="team") + ) + + class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(unique=True) + team_id: Optional[int] = Field(default=None, foreign_key="team.id") + team: Optional[Team] = Relationship( + sa_relationship=RelationshipProperty("Team", back_populates="heroes") + ) + + team_preventers = Team(name="Preventers") + hero_rusty_man = Hero(name="Rusty-Man", team=team_preventers) + + engine = create_engine("sqlite://", echo=True) + + SQLModel.metadata.create_all(engine) + + with Session(engine) as session: + session.add(hero_rusty_man) + session.commit() + session.refresh(hero_rusty_man) + # The next statement should not raise an AttributeError + assert hero_rusty_man.team + assert hero_rusty_man.team.name == "Preventers" From d939b7c45c394fb60d04e9f3554cb11519914b5d Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 22 Oct 2023 12:02:24 +0000 Subject: [PATCH 211/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 1f52793ea4..5808054382 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix SQLAlchemy version 1.4.36 breaks SQLModel relationships (#315). PR [#461](https://github.com/tiangolo/sqlmodel/pull/461) by [@byrman](https://github.com/byrman). * 🛠️ Add `CITATION.cff` file for academic citations. PR [#13](https://github.com/tiangolo/sqlmodel/pull/13) by [@sugatoray](https://github.com/sugatoray). * 👷 Update docs deployments to Cloudflare. PR [#630](https://github.com/tiangolo/sqlmodel/pull/630) by [@tiangolo](https://github.com/tiangolo). * 👷‍♂️ Upgrade CI for docs. PR [#628](https://github.com/tiangolo/sqlmodel/pull/628) by [@tiangolo](https://github.com/tiangolo). From bdcf11bca62889710678913e7fc7ae30e067d0ae Mon Sep 17 00:00:00 2001 From: Jorge Alvarado Date: Sun, 22 Oct 2023 09:03:51 -0300 Subject: [PATCH 212/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20instructions=20?= =?UTF-8?q?about=20how=20to=20make=20a=20foreign=20key=20required=20in=20`?= =?UTF-8?q?docs/tutorial/relationship-attributes/define-relationships-attr?= =?UTF-8?q?ibutes.md`=20(#474)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- .../define-relationships-attributes.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/tutorial/relationship-attributes/define-relationships-attributes.md b/docs/tutorial/relationship-attributes/define-relationships-attributes.md index 0531ec53e5..b6e77d9b45 100644 --- a/docs/tutorial/relationship-attributes/define-relationships-attributes.md +++ b/docs/tutorial/relationship-attributes/define-relationships-attributes.md @@ -115,9 +115,7 @@ This means that this attribute could be `None`, or it could be a full `Team` obj This is because the related **`team_id` could also be `None`** (or `NULL` in the database). -If it was required for a `Hero` instance to belong to a `Team`, then the `team_id` would be `int` instead of `Optional[int]`. - -And the `team` attribute would be a `Team` instead of `Optional[Team]`. +If it was required for a `Hero` instance to belong to a `Team`, then the `team_id` would be `int` instead of `Optional[int]`, its `Field` would be `Field(foreign_key="team.id")` instead of `Field(default=None, foreign_key="team.id")` and the `team` attribute would be a `Team` instead of `Optional[Team]`. ## Relationship Attributes With Lists From b7d6a0a3c39d6ddddb8eb7e3d9d8c90f47b954fc Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 22 Oct 2023 12:04:24 +0000 Subject: [PATCH 213/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 5808054382..6b8e3ffbaf 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Update instructions about how to make a foreign key required in `docs/tutorial/relationship-attributes/define-relationships-attributes.md`. PR [#474](https://github.com/tiangolo/sqlmodel/pull/474) by [@jalvaradosegura](https://github.com/jalvaradosegura). * 🐛 Fix SQLAlchemy version 1.4.36 breaks SQLModel relationships (#315). PR [#461](https://github.com/tiangolo/sqlmodel/pull/461) by [@byrman](https://github.com/byrman). * 🛠️ Add `CITATION.cff` file for academic citations. PR [#13](https://github.com/tiangolo/sqlmodel/pull/13) by [@sugatoray](https://github.com/sugatoray). * 👷 Update docs deployments to Cloudflare. PR [#630](https://github.com/tiangolo/sqlmodel/pull/630) by [@tiangolo](https://github.com/tiangolo). From a2e2942aadba6caea9a34461cd48fe89cdfe2cf1 Mon Sep 17 00:00:00 2001 From: Ben Rapaport <89947784+br-follow@users.noreply.github.com> Date: Sun, 22 Oct 2023 08:26:37 -0400 Subject: [PATCH 214/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= =?UTF-8?q?,=20add=20second=20author=20to=20PR=20(#429)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 6b8e3ffbaf..b6ee0be691 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -40,7 +40,7 @@ ### Fixes -* 🐛 Fix auto detecting and setting `nullable`, allowing overrides in field. PR [#423](https://github.com/tiangolo/sqlmodel/pull/423) by [@JonasKs](https://github.com/JonasKs). +* 🐛 Fix auto detecting and setting `nullable`, allowing overrides in field. PR [#423](https://github.com/tiangolo/sqlmodel/pull/423) by [@JonasKs](https://github.com/JonasKs) and [@br-follow](https://github.com/br-follow). * ♻️ Update `expresion.py`, sync from Jinja2 template, implement `inherit_cache` to solve errors like: `SAWarning: Class SelectOfScalar will not make use of SQL compilation caching`. PR [#422](https://github.com/tiangolo/sqlmodel/pull/422) by [@tiangolo](https://github.com/tiangolo). ### Docs From 2799303f6c39f4d1ae35a4c90adf4654c21392b9 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 22 Oct 2023 12:27:13 +0000 Subject: [PATCH 215/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index b6ee0be691..32a8569057 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Update release notes, add second author to PR. PR [#429](https://github.com/tiangolo/sqlmodel/pull/429) by [@br-follow](https://github.com/br-follow). * 📝 Update instructions about how to make a foreign key required in `docs/tutorial/relationship-attributes/define-relationships-attributes.md`. PR [#474](https://github.com/tiangolo/sqlmodel/pull/474) by [@jalvaradosegura](https://github.com/jalvaradosegura). * 🐛 Fix SQLAlchemy version 1.4.36 breaks SQLModel relationships (#315). PR [#461](https://github.com/tiangolo/sqlmodel/pull/461) by [@byrman](https://github.com/byrman). * 🛠️ Add `CITATION.cff` file for academic citations. PR [#13](https://github.com/tiangolo/sqlmodel/pull/13) by [@sugatoray](https://github.com/sugatoray). From 893d64fd3127b1737922629cb3b7803fdb370947 Mon Sep 17 00:00:00 2001 From: Dipendra Raj Panta <49410574+Mr-DRP@users.noreply.github.com> Date: Sun, 22 Oct 2023 18:31:52 +0545 Subject: [PATCH 216/906] =?UTF-8?q?=F0=9F=93=9D=20Fix=20typos=20(duplicati?= =?UTF-8?q?on)=20in=20main=20page=20(#631)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update README.md * Update index.md --- README.md | 2 +- docs/index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f85bb97a6d..a9387c510a 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ It combines SQLAlchemy and Pydantic and tries to simplify the code you write as ## Requirements -A recent and currently supported version of Python Python. +A recent and currently supported version of Python. As **SQLModel** is based on **Pydantic** and **SQLAlchemy**, it requires them. They will be automatically installed when you install SQLModel. diff --git a/docs/index.md b/docs/index.md index f85bb97a6d..a9387c510a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -50,7 +50,7 @@ It combines SQLAlchemy and Pydantic and tries to simplify the code you write as ## Requirements -A recent and currently supported version of Python Python. +A recent and currently supported version of Python. As **SQLModel** is based on **Pydantic** and **SQLAlchemy**, it requires them. They will be automatically installed when you install SQLModel. From 357417e6d581dd66adfbe4efa667b5139ef161ff Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 22 Oct 2023 12:47:27 +0000 Subject: [PATCH 217/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 32a8569057..d15ce9771b 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Fix typos (duplication) in main page. PR [#631](https://github.com/tiangolo/sqlmodel/pull/631) by [@Mr-DRP](https://github.com/Mr-DRP). * 📝 Update release notes, add second author to PR. PR [#429](https://github.com/tiangolo/sqlmodel/pull/429) by [@br-follow](https://github.com/br-follow). * 📝 Update instructions about how to make a foreign key required in `docs/tutorial/relationship-attributes/define-relationships-attributes.md`. PR [#474](https://github.com/tiangolo/sqlmodel/pull/474) by [@jalvaradosegura](https://github.com/jalvaradosegura). * 🐛 Fix SQLAlchemy version 1.4.36 breaks SQLModel relationships (#315). PR [#461](https://github.com/tiangolo/sqlmodel/pull/461) by [@byrman](https://github.com/byrman). From 1568bad01ec1d5f7edada00e49c18ce44ba0a334 Mon Sep 17 00:00:00 2001 From: Kian-Meng Ang Date: Sun, 22 Oct 2023 20:50:44 +0800 Subject: [PATCH 218/906] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typos=20foun?= =?UTF-8?q?d=20with=20codespell=20(#520)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found via `codespell -S *.svg,*.css,*.js,*.drawio -L pullrequest,sesion` --- docs/tutorial/automatic-id-none-refresh.md | 14 +++++++------- docs/tutorial/code-structure.md | 2 +- docs/tutorial/connect/create-connected-tables.md | 2 +- .../create-db-and-table-with-db-browser.md | 2 +- docs/tutorial/fastapi/tests.md | 2 +- docs/tutorial/index.md | 4 ++-- docs/tutorial/insert.md | 2 +- docs/tutorial/many-to-many/create-data.md | 2 +- docs/tutorial/one.md | 2 +- .../relationship-attributes/back-populates.md | 2 +- .../relationship-attributes/read-relationships.md | 2 +- docs/tutorial/select.md | 4 ++-- docs/tutorial/where.md | 4 ++-- .../annotations/en/tutorial002.md | 2 +- .../tutorial/update/annotations/en/tutorial004.md | 2 +- sqlmodel/default.py | 4 ++-- tests/test_validation.py | 2 +- 17 files changed, 27 insertions(+), 27 deletions(-) diff --git a/docs/tutorial/automatic-id-none-refresh.md b/docs/tutorial/automatic-id-none-refresh.md index bbf74dd307..d41cd14e91 100644 --- a/docs/tutorial/automatic-id-none-refresh.md +++ b/docs/tutorial/automatic-id-none-refresh.md @@ -36,7 +36,7 @@ When we create a new `Hero` instance, we don't set the `id`: {!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:23-26]!} -# Code below ommitted 👇 +# Code below omitted 👇 ```
@@ -125,7 +125,7 @@ We can verify by creating a session using a `with` block and adding the objects. {!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:23-41]!} -# Code below ommitted 👇 +# Code below omitted 👇 ```
@@ -238,7 +238,7 @@ To confirm and understand how this **automatic expiration and refresh** of data {!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:33-58]!} -# Code below ommitted 👇 +# Code below omitted 👇 ```
@@ -271,7 +271,7 @@ Let's see how it works: ```console $ python app.py -// Output above ommitted 👆 +// Output above omitted 👆 // After committing, the objects are expired and have no values After committing the session @@ -335,7 +335,7 @@ You can do that too with `session.refresh(object)`: {!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:33-67]!} -# Code below ommitted 👇 +# Code below omitted 👇 ```
@@ -362,7 +362,7 @@ Here's how the output would look like: ```console $ python app.py -// Output above ommitted 👆 +// Output above omitted 👆 // The first refresh INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age @@ -427,7 +427,7 @@ And the output shows again the same data: ```console $ python app.py -// Output above ommitted 👆 +// Output above omitted 👆 // By finishing the with block, the Session is closed, including a rollback of any pending transaction that could have been there and was not committed INFO Engine ROLLBACK diff --git a/docs/tutorial/code-structure.md b/docs/tutorial/code-structure.md index 59a9e4bd9a..502c8bf958 100644 --- a/docs/tutorial/code-structure.md +++ b/docs/tutorial/code-structure.md @@ -168,7 +168,7 @@ Let's assume that now the file structure is: ### Circular Imports and Type Annotations -The problem with circular imports is that Python can't resolve them at *runtime*. +The problem with circular imports is that Python can't resolve them at *runtime*. But when using Python **type annotations** it's very common to need to declare the type of some variables with classes imported from other files. diff --git a/docs/tutorial/connect/create-connected-tables.md b/docs/tutorial/connect/create-connected-tables.md index 452c904ebe..14a310f59d 100644 --- a/docs/tutorial/connect/create-connected-tables.md +++ b/docs/tutorial/connect/create-connected-tables.md @@ -106,7 +106,7 @@ This is the same model we have been using up to now, we are just adding the new Most of that should look familiar: -The column will be named `team_id`. It will be an integer, and it could be `NULL` in the database (or `None` in Python), becase there could be some heroes that don't belong to any team. +The column will be named `team_id`. It will be an integer, and it could be `NULL` in the database (or `None` in Python), because there could be some heroes that don't belong to any team. We add a default of `None` to the `Field()` so we don't have to explicitly pass `team_id=None` when creating a hero. diff --git a/docs/tutorial/create-db-and-table-with-db-browser.md b/docs/tutorial/create-db-and-table-with-db-browser.md index a1bb394024..4437f15a6d 100644 --- a/docs/tutorial/create-db-and-table-with-db-browser.md +++ b/docs/tutorial/create-db-and-table-with-db-browser.md @@ -164,6 +164,6 @@ Of course, you can also go and take a full SQL course or read a book about SQL, We saw how to interact with SQLite databases in files using **DB Browser for SQLite** in a visual user interface. -We also saw how to use it to write some SQL directly to the SQLite database. This will be useful to verify the data in the database is looking correclty, to debug, etc. +We also saw how to use it to write some SQL directly to the SQLite database. This will be useful to verify the data in the database is looking correctly, to debug, etc. In the next chapters we will start using **SQLModel** to interact with the database, and we will continue to use **DB Browser for SQLite** at the same time to look at the database underneath. 🔍 diff --git a/docs/tutorial/fastapi/tests.md b/docs/tutorial/fastapi/tests.md index f817a883a1..cc6ad65c6f 100644 --- a/docs/tutorial/fastapi/tests.md +++ b/docs/tutorial/fastapi/tests.md @@ -82,7 +82,7 @@ But now, we need to deal with a bit of logistics and details we are not paying a This test looks fine, but there's a problem. -If we run it, it will use the same **production database** that we are using to store our very important **heroes**, and we will end up adding unnecesary data to it, or even worse, in future tests we could end up removing production data. +If we run it, it will use the same **production database** that we are using to store our very important **heroes**, and we will end up adding unnecessary data to it, or even worse, in future tests we could end up removing production data. So, we should use an independent **testing database**, just for the tests. diff --git a/docs/tutorial/index.md b/docs/tutorial/index.md index 79fa670cfd..e8594148d6 100644 --- a/docs/tutorial/index.md +++ b/docs/tutorial/index.md @@ -135,7 +135,7 @@ Here are the commands you could use: // Remember that you might need to use python3.9 or similar 💡 // Create the virtual environment using the module "venv" $ python3 -m venv env - // ...here it creates the virtual enviroment in the directory "env" + // ...here it creates the virtual environment in the directory "env" // Activate the virtual environment $ source ./env/bin/activate // Verify that the virtual environment is active @@ -157,7 +157,7 @@ Here are the commands you could use: ```console // Create the virtual environment using the module "venv" # >$ python3 -m venv env - // ...here it creates the virtual enviroment in the directory "env" + // ...here it creates the virtual environment in the directory "env" // Activate the virtual environment # >$ .\env\Scripts\Activate.ps1 // Verify that the virtual environment is active diff --git a/docs/tutorial/insert.md b/docs/tutorial/insert.md index 5947e1e5dc..ecf87adbad 100644 --- a/docs/tutorial/insert.md +++ b/docs/tutorial/insert.md @@ -171,7 +171,7 @@ The first step is to import the `Session` class: ```Python hl_lines="3" {!./docs_src/tutorial/insert/tutorial001.py[ln:1-3]!} -# Code below ommitted 👇 +# Code below omitted 👇 ```
diff --git a/docs/tutorial/many-to-many/create-data.md b/docs/tutorial/many-to-many/create-data.md index 22afb7ce7e..2a51f5acae 100644 --- a/docs/tutorial/many-to-many/create-data.md +++ b/docs/tutorial/many-to-many/create-data.md @@ -179,4 +179,4 @@ INFO Engine ROLLBACK ## Recap -After setting up the model link, using it with **relationship attributes** is fairly straighforward, just Python objects. ✨ +After setting up the model link, using it with **relationship attributes** is fairly straightforward, just Python objects. ✨ diff --git a/docs/tutorial/one.md b/docs/tutorial/one.md index 3b60653ed9..f06343f67f 100644 --- a/docs/tutorial/one.md +++ b/docs/tutorial/one.md @@ -12,7 +12,7 @@ Let's see the utilities to read a single row. ## Continue From Previous Code -We'll continue with the same examples we have been using in the previous chapters to create and select data and we'll keep udpating them. +We'll continue with the same examples we have been using in the previous chapters to create and select data and we'll keep updating them.
👀 Full file preview diff --git a/docs/tutorial/relationship-attributes/back-populates.md b/docs/tutorial/relationship-attributes/back-populates.md index 86a4c2a70a..27129a066c 100644 --- a/docs/tutorial/relationship-attributes/back-populates.md +++ b/docs/tutorial/relationship-attributes/back-populates.md @@ -123,7 +123,7 @@ Now let's update **Spider-Boy**, removing him from the team by setting `hero_spi
-The first important thing is, we *haven't commited* the hero yet, so accessing the list of heroes would not trigger an automatic refresh. +The first important thing is, we *haven't committed* the hero yet, so accessing the list of heroes would not trigger an automatic refresh. But in our code, in this exact point in time, we already said that **Spider-Boy** is no longer part of the **Preventers**. 🔥 diff --git a/docs/tutorial/relationship-attributes/read-relationships.md b/docs/tutorial/relationship-attributes/read-relationships.md index 181b229589..c970b5eb06 100644 --- a/docs/tutorial/relationship-attributes/read-relationships.md +++ b/docs/tutorial/relationship-attributes/read-relationships.md @@ -52,7 +52,7 @@ With what we have learned **up to now**, we could use a `select()` statement, th ## Get Relationship Team - New Way -But now that we have the **relationship attributes**, we can just access them, and **SQLModel** (actually SQLAlchemy) will go and fetch the correspoinding data from the database, and make it available in the attribute. ✨ +But now that we have the **relationship attributes**, we can just access them, and **SQLModel** (actually SQLAlchemy) will go and fetch the corresponding data from the database, and make it available in the attribute. ✨ So, the highlighted block above, has the same results as the block below: diff --git a/docs/tutorial/select.md b/docs/tutorial/select.md index fb638c1212..5de32db5a1 100644 --- a/docs/tutorial/select.md +++ b/docs/tutorial/select.md @@ -190,7 +190,7 @@ First we have to import `select` from `sqlmodel` at the top of the file: ```Python hl_lines="3" {!./docs_src/tutorial/select/tutorial001.py[ln:1-3]!} -# More code below ommitted 👇 +# More code below omitted 👇 ```
@@ -472,7 +472,7 @@ SQLAlchemy's own `Session` has a method `session.execute()`. It doesn't have a ` If you see SQLAlchemy tutorials, they will always use `session.execute()`. -**SQLModel**'s own `Session` inherits directly from SQLAlchemy's `Session`, and adds this additonal method `session.exec()`. Underneath, it uses the same `session.execute()`. +**SQLModel**'s own `Session` inherits directly from SQLAlchemy's `Session`, and adds this additional method `session.exec()`. Underneath, it uses the same `session.execute()`. But `session.exec()` does several **tricks** combined with the tricks in `session()` to give you the **best editor support**, with **autocompletion** and **inline errors** everywhere, even after getting data from a select. ✨ diff --git a/docs/tutorial/where.md b/docs/tutorial/where.md index ca85a4dd00..47f1b9b113 100644 --- a/docs/tutorial/where.md +++ b/docs/tutorial/where.md @@ -206,7 +206,7 @@ We care specially about the **select** statement: ## Filter Rows Using `WHERE` with **SQLModel** -Now, the same way that we add `WHERE` to a SQL statement to filter rows, we can add a `.where()` to a **SQLModel** `select()` statment to filter rows, which will filter the objects returned: +Now, the same way that we add `WHERE` to a SQL statement to filter rows, we can add a `.where()` to a **SQLModel** `select()` statement to filter rows, which will filter the objects returned: ```Python hl_lines="5" # Code above omitted 👆 @@ -748,7 +748,7 @@ FROM hero WHERE hero.age >= ? AND hero.age < ? INFO Engine [no key 0.00014s] (35, 40) -// The two heros printed +// The two heroes printed age=35 id=5 name='Black Lion' secret_name='Trevor Challa' age=36 id=6 name='Dr. Weird' secret_name='Steve Weird' diff --git a/docs_src/tutorial/automatic_id_none_refresh/annotations/en/tutorial002.md b/docs_src/tutorial/automatic_id_none_refresh/annotations/en/tutorial002.md index fd33fec778..8306d9b898 100644 --- a/docs_src/tutorial/automatic_id_none_refresh/annotations/en/tutorial002.md +++ b/docs_src/tutorial/automatic_id_none_refresh/annotations/en/tutorial002.md @@ -157,7 +157,7 @@ Hero 3: ``` -21. Print the line `"After commiting the session, show IDs"`. +21. Print the line `"After committing the session, show IDs"`. Generates the output: diff --git a/docs_src/tutorial/update/annotations/en/tutorial004.md b/docs_src/tutorial/update/annotations/en/tutorial004.md index 55755cd88d..3fcf1040ec 100644 --- a/docs_src/tutorial/update/annotations/en/tutorial004.md +++ b/docs_src/tutorial/update/annotations/en/tutorial004.md @@ -132,7 +132,7 @@ !!! tip SQLAlchemy is still using the previous transaction, so it doesn't have to create a new one. -18. Print the first hero, now udpated. +18. Print the first hero, now updated. This generates the output: diff --git a/sqlmodel/default.py b/sqlmodel/default.py index bb44972e24..e8e37a5566 100644 --- a/sqlmodel/default.py +++ b/sqlmodel/default.py @@ -6,7 +6,7 @@ class _DefaultPlaceholder: You shouldn't use this class directly. It's used internally to recognize when a default value has been overwritten, even - if the overriden default value was truthy. + if the overridden default value was truthy. """ def __init__(self, value: Any): @@ -27,6 +27,6 @@ def Default(value: _TDefaultType) -> _TDefaultType: You shouldn't use this function directly. It's used internally to recognize when a default value has been overwritten, even - if the overriden default value was truthy. + if the overridden default value was truthy. """ return _DefaultPlaceholder(value) # type: ignore diff --git a/tests/test_validation.py b/tests/test_validation.py index a3ff6e39ba..ad60fcb945 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -7,7 +7,7 @@ def test_validation(clear_sqlmodel): - """Test validation of implicit and explict None values. + """Test validation of implicit and explicit None values. # For consistency with pydantic, validators are not to be called on # arguments that are not explicitly provided. From 5231c8b6bb748906fbd6cf490ad477ff898ec8f0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 22 Oct 2023 12:51:20 +0000 Subject: [PATCH 219/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index d15ce9771b..033a8ce99f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏️ Fix typos found with codespell. PR [#520](https://github.com/tiangolo/sqlmodel/pull/520) by [@kianmeng](https://github.com/kianmeng). * 📝 Fix typos (duplication) in main page. PR [#631](https://github.com/tiangolo/sqlmodel/pull/631) by [@Mr-DRP](https://github.com/Mr-DRP). * 📝 Update release notes, add second author to PR. PR [#429](https://github.com/tiangolo/sqlmodel/pull/429) by [@br-follow](https://github.com/br-follow). * 📝 Update instructions about how to make a foreign key required in `docs/tutorial/relationship-attributes/define-relationships-attributes.md`. PR [#474](https://github.com/tiangolo/sqlmodel/pull/474) by [@jalvaradosegura](https://github.com/jalvaradosegura). From 8970833b147e998b5e5d0c52421a979d4ac81cf8 Mon Sep 17 00:00:00 2001 From: Matvey Fedoseev <41521530+MatveyF@users.noreply.github.com> Date: Sun, 22 Oct 2023 13:54:36 +0100 Subject: [PATCH 220/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20outdated=20link?= =?UTF-8?q?=20in=20`docs/db-to-code.md`=20(#649)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/db-to-code.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/db-to-code.md b/docs/db-to-code.md index 2e0fb1babc..980c457148 100644 --- a/docs/db-to-code.md +++ b/docs/db-to-code.md @@ -111,7 +111,7 @@ DROP TABLE hero; That is how you tell the database in SQL to delete the entire table `hero`. -Nooooo! We lost all the data in the `hero` table! 💥😱 +Nooooo! We lost all the data in the `hero` table! 💥😱 ### SQL Sanitization @@ -305,4 +305,4 @@ You will see **your own code** a lot more than the internal table names, so it's So, to keep things consistent, I'll keep using the same table names that **SQLModel** would have generated. !!! tip - You can also override the table name. You can read about it in the Advanced User Guide. \ No newline at end of file + You can also override the table name. You can read about it in the Advanced User Guide. From a8a792e3c0c43f8c0ff443d54c3353c05c7c437b Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 22 Oct 2023 12:55:08 +0000 Subject: [PATCH 221/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 033a8ce99f..f5660af284 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Update outdated link in `docs/db-to-code.md`. PR [#649](https://github.com/tiangolo/sqlmodel/pull/649) by [@MatveyF](https://github.com/MatveyF). * ✏️ Fix typos found with codespell. PR [#520](https://github.com/tiangolo/sqlmodel/pull/520) by [@kianmeng](https://github.com/kianmeng). * 📝 Fix typos (duplication) in main page. PR [#631](https://github.com/tiangolo/sqlmodel/pull/631) by [@Mr-DRP](https://github.com/Mr-DRP). * 📝 Update release notes, add second author to PR. PR [#429](https://github.com/tiangolo/sqlmodel/pull/429) by [@br-follow](https://github.com/br-follow). From 840fd08ab2f803d4e8fb67c7587a59621473c715 Mon Sep 17 00:00:00 2001 From: David Danier Date: Mon, 23 Oct 2023 08:42:30 +0200 Subject: [PATCH 222/906] =?UTF-8?q?=E2=9C=A8=20Raise=20a=20more=20clear=20?= =?UTF-8?q?error=20when=20a=20type=20is=20not=20valid=20(#425)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- sqlmodel/main.py | 79 ++++++++++++++-------------- tests/test_sqlalchemy_type_errors.py | 28 ++++++++++ 2 files changed, 68 insertions(+), 39 deletions(-) create mode 100644 tests/test_sqlalchemy_type_errors.py diff --git a/sqlmodel/main.py b/sqlmodel/main.py index caae8cf08d..7dec60ddac 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -374,45 +374,46 @@ def __init__( def get_sqlalchemy_type(field: ModelField) -> Any: - if issubclass(field.type_, str): - if field.field_info.max_length: - return AutoString(length=field.field_info.max_length) - return AutoString - if issubclass(field.type_, float): - return Float - if issubclass(field.type_, bool): - return Boolean - if issubclass(field.type_, int): - return Integer - if issubclass(field.type_, datetime): - return DateTime - if issubclass(field.type_, date): - return Date - if issubclass(field.type_, timedelta): - return Interval - if issubclass(field.type_, time): - return Time - if issubclass(field.type_, Enum): - return sa_Enum(field.type_) - if issubclass(field.type_, bytes): - return LargeBinary - if issubclass(field.type_, Decimal): - return Numeric( - precision=getattr(field.type_, "max_digits", None), - scale=getattr(field.type_, "decimal_places", None), - ) - if issubclass(field.type_, ipaddress.IPv4Address): - return AutoString - if issubclass(field.type_, ipaddress.IPv4Network): - return AutoString - if issubclass(field.type_, ipaddress.IPv6Address): - return AutoString - if issubclass(field.type_, ipaddress.IPv6Network): - return AutoString - if issubclass(field.type_, Path): - return AutoString - if issubclass(field.type_, uuid.UUID): - return GUID + if isinstance(field.type_, type) and field.shape == SHAPE_SINGLETON: + if issubclass(field.type_, str): + if field.field_info.max_length: + return AutoString(length=field.field_info.max_length) + return AutoString + if issubclass(field.type_, float): + return Float + if issubclass(field.type_, bool): + return Boolean + if issubclass(field.type_, int): + return Integer + if issubclass(field.type_, datetime): + return DateTime + if issubclass(field.type_, date): + return Date + if issubclass(field.type_, timedelta): + return Interval + if issubclass(field.type_, time): + return Time + if issubclass(field.type_, Enum): + return sa_Enum(field.type_) + if issubclass(field.type_, bytes): + return LargeBinary + if issubclass(field.type_, Decimal): + return Numeric( + precision=getattr(field.type_, "max_digits", None), + scale=getattr(field.type_, "decimal_places", None), + ) + if issubclass(field.type_, ipaddress.IPv4Address): + return AutoString + if issubclass(field.type_, ipaddress.IPv4Network): + return AutoString + if issubclass(field.type_, ipaddress.IPv6Address): + return AutoString + if issubclass(field.type_, ipaddress.IPv6Network): + return AutoString + if issubclass(field.type_, Path): + return AutoString + if issubclass(field.type_, uuid.UUID): + return GUID raise ValueError(f"The field {field.name} has no matching SQLAlchemy type") diff --git a/tests/test_sqlalchemy_type_errors.py b/tests/test_sqlalchemy_type_errors.py new file mode 100644 index 0000000000..e211c46a34 --- /dev/null +++ b/tests/test_sqlalchemy_type_errors.py @@ -0,0 +1,28 @@ +from typing import Any, Dict, List, Optional, Union + +import pytest +from sqlmodel import Field, SQLModel + + +def test_type_list_breaks() -> None: + with pytest.raises(ValueError): + + class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + tags: List[str] + + +def test_type_dict_breaks() -> None: + with pytest.raises(ValueError): + + class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + tags: Dict[str, Any] + + +def test_type_union_breaks() -> None: + with pytest.raises(ValueError): + + class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + tags: Union[int, str] From 9ba303910639a446d428aa9e1a4a4b54d79ca19c Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 23 Oct 2023 06:43:04 +0000 Subject: [PATCH 223/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index f5660af284..8ba2f57a69 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Raise a more clear error when a type is not valid. PR [#425](https://github.com/tiangolo/sqlmodel/pull/425) by [@ddanier](https://github.com/ddanier). * 📝 Update outdated link in `docs/db-to-code.md`. PR [#649](https://github.com/tiangolo/sqlmodel/pull/649) by [@MatveyF](https://github.com/MatveyF). * ✏️ Fix typos found with codespell. PR [#520](https://github.com/tiangolo/sqlmodel/pull/520) by [@kianmeng](https://github.com/kianmeng). * 📝 Fix typos (duplication) in main page. PR [#631](https://github.com/tiangolo/sqlmodel/pull/631) by [@Mr-DRP](https://github.com/Mr-DRP). From 065fcdc8280ab333a0e165eec9124b727d2a92e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 23 Oct 2023 11:34:50 +0400 Subject: [PATCH 224/906] =?UTF-8?q?=F0=9F=91=B7=20Move=20to=20Ruff=20and?= =?UTF-8?q?=20add=20pre-commit=20(#661)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 👷 Add pre-commit * 🔧 Add pyproject.toml config for Ruff * ➕ Replace isort, flake8, autoflake with Ruff * 🔨 Update lint and format scripts * 🎨 Format with Ruff * 🔧 Update Poetry config --- .pre-commit-config.yaml | 35 +++++++++++++++++++++++ pyproject.toml | 49 +++++++++++++++++++------------- scripts/format.sh | 5 ++-- scripts/lint.sh | 3 +- sqlmodel/__init__.py | 62 ++++++++++++++++++++--------------------- sqlmodel/main.py | 29 ++++++++++++------- 6 files changed, 116 insertions(+), 67 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..9f7085f72f --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,35 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +default_language_version: + python: python3.10 +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-added-large-files + - id: check-toml + - id: check-yaml + args: + - --unsafe + - id: end-of-file-fixer + - id: trailing-whitespace +- repo: https://github.com/asottile/pyupgrade + rev: v3.7.0 + hooks: + - id: pyupgrade + args: + - --py3-plus + - --keep-runtime-typing +- repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.0.275 + hooks: + - id: ruff + args: + - --fix +- repo: https://github.com/psf/black + rev: 23.3.0 + hooks: + - id: black +ci: + autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks + autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate diff --git a/pyproject.toml b/pyproject.toml index e402727150..73d8b3ac92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,10 +35,9 @@ SQLAlchemy = ">=1.4.17,<=1.4.41" pydantic = "^1.8.2" sqlalchemy2-stubs = {version = "*", allow-prereleases = true} -[tool.poetry.dev-dependencies] +[tool.poetry.group.dev.dependencies] pytest = "^7.0.1" mypy = "0.971" -flake8 = "^5.0.4" black = "^22.10.0" mkdocs = "^1.2.1" mkdocs-material = "^8.1.4" @@ -48,8 +47,7 @@ mdx-include = "^1.4.1" coverage = {extras = ["toml"], version = "^6.2"} fastapi = "^0.68.1" requests = "^2.26.0" -autoflake = "^1.4" -isort = "^5.9.3" +ruff = "^0.1.1" [build-system] requires = ["poetry-core"] @@ -75,27 +73,19 @@ exclude_lines = [ "if TYPE_CHECKING:", ] -[tool.isort] -profile = "black" -known_third_party = ["sqlmodel"] -skip_glob = [ - "sqlmodel/__init__.py", - ] - - [tool.mypy] # --strict disallow_any_generics = true -disallow_subclassing_any = true -disallow_untyped_calls = true +disallow_subclassing_any = true +disallow_untyped_calls = true disallow_untyped_defs = true -disallow_incomplete_defs = true -check_untyped_defs = true -disallow_untyped_decorators = true +disallow_incomplete_defs = true +check_untyped_defs = true +disallow_untyped_decorators = true no_implicit_optional = true -warn_redundant_casts = true +warn_redundant_casts = true warn_unused_ignores = true -warn_return_any = true +warn_return_any = true implicit_reexport = false strict_equality = true # --strict end @@ -104,4 +94,23 @@ strict_equality = true module = "sqlmodel.sql.expression" warn_unused_ignores = false -# invalidate CI cache: 1 +[tool.ruff] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "C", # flake8-comprehensions + "B", # flake8-bugbear +] +ignore = [ + "E501", # line too long, handled by black + "B008", # do not perform function calls in argument defaults + "C901", # too complex +] + +[tool.ruff.per-file-ignores] +# "__init__.py" = ["F401"] + +[tool.ruff.isort] +known-third-party = ["sqlmodel", "sqlalchemy", "pydantic", "fastapi"] diff --git a/scripts/format.sh b/scripts/format.sh index 0d456398fb..b6aebd10d4 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -1,6 +1,5 @@ #!/bin/sh -e set -x -autoflake --remove-all-unused-imports --recursive --remove-unused-variables --in-place sqlmodel docs_src tests --exclude=__init__.py -black sqlmodel tests docs_src -isort sqlmodel tests docs_src +ruff sqlmodel tests docs_src scripts --fix +black sqlmodel tests docs_src scripts diff --git a/scripts/lint.sh b/scripts/lint.sh index 4191d90f1f..b328e3d9ac 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -4,6 +4,5 @@ set -e set -x mypy sqlmodel -flake8 sqlmodel tests docs_src +ruff sqlmodel tests docs_src scripts black sqlmodel tests docs_src --check -isort sqlmodel tests docs_src scripts --check-only diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index 720aa8c929..3aa6e0d2ac 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -5,12 +5,12 @@ from sqlalchemy.engine import engine_from_config as engine_from_config from sqlalchemy.inspection import inspect as inspect from sqlalchemy.schema import BLANK_SCHEMA as BLANK_SCHEMA +from sqlalchemy.schema import DDL as DDL from sqlalchemy.schema import CheckConstraint as CheckConstraint from sqlalchemy.schema import Column as Column from sqlalchemy.schema import ColumnDefault as ColumnDefault from sqlalchemy.schema import Computed as Computed from sqlalchemy.schema import Constraint as Constraint -from sqlalchemy.schema import DDL as DDL from sqlalchemy.schema import DefaultClause as DefaultClause from sqlalchemy.schema import FetchedValue as FetchedValue from sqlalchemy.schema import ForeignKey as ForeignKey @@ -23,6 +23,14 @@ from sqlalchemy.schema import Table as Table from sqlalchemy.schema import ThreadLocalMetaData as ThreadLocalMetaData from sqlalchemy.schema import UniqueConstraint as UniqueConstraint +from sqlalchemy.sql import LABEL_STYLE_DEFAULT as LABEL_STYLE_DEFAULT +from sqlalchemy.sql import ( + LABEL_STYLE_DISAMBIGUATE_ONLY as LABEL_STYLE_DISAMBIGUATE_ONLY, +) +from sqlalchemy.sql import LABEL_STYLE_NONE as LABEL_STYLE_NONE +from sqlalchemy.sql import ( + LABEL_STYLE_TABLENAME_PLUS_COL as LABEL_STYLE_TABLENAME_PLUS_COL, +) from sqlalchemy.sql import alias as alias from sqlalchemy.sql import all_ as all_ from sqlalchemy.sql import and_ as and_ @@ -48,14 +56,6 @@ from sqlalchemy.sql import intersect as intersect from sqlalchemy.sql import intersect_all as intersect_all from sqlalchemy.sql import join as join -from sqlalchemy.sql import LABEL_STYLE_DEFAULT as LABEL_STYLE_DEFAULT -from sqlalchemy.sql import ( - LABEL_STYLE_DISAMBIGUATE_ONLY as LABEL_STYLE_DISAMBIGUATE_ONLY, -) -from sqlalchemy.sql import LABEL_STYLE_NONE as LABEL_STYLE_NONE -from sqlalchemy.sql import ( - LABEL_STYLE_TABLENAME_PLUS_COL as LABEL_STYLE_TABLENAME_PLUS_COL, -) from sqlalchemy.sql import lambda_stmt as lambda_stmt from sqlalchemy.sql import lateral as lateral from sqlalchemy.sql import literal as literal @@ -85,55 +85,53 @@ from sqlalchemy.sql import within_group as within_group from sqlalchemy.types import ARRAY as ARRAY from sqlalchemy.types import BIGINT as BIGINT -from sqlalchemy.types import BigInteger as BigInteger from sqlalchemy.types import BINARY as BINARY from sqlalchemy.types import BLOB as BLOB from sqlalchemy.types import BOOLEAN as BOOLEAN -from sqlalchemy.types import Boolean as Boolean from sqlalchemy.types import CHAR as CHAR from sqlalchemy.types import CLOB as CLOB from sqlalchemy.types import DATE as DATE -from sqlalchemy.types import Date as Date from sqlalchemy.types import DATETIME as DATETIME -from sqlalchemy.types import DateTime as DateTime from sqlalchemy.types import DECIMAL as DECIMAL -from sqlalchemy.types import Enum as Enum from sqlalchemy.types import FLOAT as FLOAT -from sqlalchemy.types import Float as Float from sqlalchemy.types import INT as INT from sqlalchemy.types import INTEGER as INTEGER -from sqlalchemy.types import Integer as Integer -from sqlalchemy.types import Interval as Interval from sqlalchemy.types import JSON as JSON -from sqlalchemy.types import LargeBinary as LargeBinary from sqlalchemy.types import NCHAR as NCHAR from sqlalchemy.types import NUMERIC as NUMERIC -from sqlalchemy.types import Numeric as Numeric from sqlalchemy.types import NVARCHAR as NVARCHAR -from sqlalchemy.types import PickleType as PickleType from sqlalchemy.types import REAL as REAL from sqlalchemy.types import SMALLINT as SMALLINT +from sqlalchemy.types import TEXT as TEXT +from sqlalchemy.types import TIME as TIME +from sqlalchemy.types import TIMESTAMP as TIMESTAMP +from sqlalchemy.types import VARBINARY as VARBINARY +from sqlalchemy.types import VARCHAR as VARCHAR +from sqlalchemy.types import BigInteger as BigInteger +from sqlalchemy.types import Boolean as Boolean +from sqlalchemy.types import Date as Date +from sqlalchemy.types import DateTime as DateTime +from sqlalchemy.types import Enum as Enum +from sqlalchemy.types import Float as Float +from sqlalchemy.types import Integer as Integer +from sqlalchemy.types import Interval as Interval +from sqlalchemy.types import LargeBinary as LargeBinary +from sqlalchemy.types import Numeric as Numeric +from sqlalchemy.types import PickleType as PickleType from sqlalchemy.types import SmallInteger as SmallInteger from sqlalchemy.types import String as String -from sqlalchemy.types import TEXT as TEXT from sqlalchemy.types import Text as Text -from sqlalchemy.types import TIME as TIME from sqlalchemy.types import Time as Time -from sqlalchemy.types import TIMESTAMP as TIMESTAMP from sqlalchemy.types import TypeDecorator as TypeDecorator from sqlalchemy.types import Unicode as Unicode from sqlalchemy.types import UnicodeText as UnicodeText -from sqlalchemy.types import VARBINARY as VARBINARY -from sqlalchemy.types import VARCHAR as VARCHAR -# Extensions and modifications of SQLAlchemy in SQLModel +# From SQLModel, modifications of SQLAlchemy or equivalents of Pydantic from .engine.create import create_engine as create_engine +from .main import Field as Field +from .main import Relationship as Relationship +from .main import SQLModel as SQLModel from .orm.session import Session as Session -from .sql.expression import select as select from .sql.expression import col as col +from .sql.expression import select as select from .sql.sqltypes import AutoString as AutoString - -# Export SQLModel specifics (equivalent to Pydantic) -from .main import SQLModel as SQLModel -from .main import Field as Field -from .main import Relationship as Relationship diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 7dec60ddac..d5a7302438 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -26,15 +26,24 @@ from pydantic import BaseConfig, BaseModel from pydantic.errors import ConfigError, DictError -from pydantic.fields import SHAPE_SINGLETON +from pydantic.fields import SHAPE_SINGLETON, ModelField, Undefined, UndefinedType 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 NoArgAnyCallable, resolve_annotations from pydantic.utils import ROOT_KEY, Representation -from sqlalchemy import Boolean, Column, Date, DateTime +from sqlalchemy import ( + Boolean, + Column, + Date, + DateTime, + Float, + ForeignKey, + Integer, + Interval, + Numeric, + inspect, +) from sqlalchemy import Enum as sa_Enum -from sqlalchemy import Float, ForeignKey, Integer, Interval, Numeric, inspect from sqlalchemy.orm import RelationshipProperty, declared_attr, registry, relationship from sqlalchemy.orm.attributes import set_attribute from sqlalchemy.orm.decl_api import DeclarativeMeta @@ -305,9 +314,9 @@ def get_config(name: str) -> Any: config_registry = cast(registry, config_registry) # If it was passed by kwargs, ensure it's also set in config new_cls.__config__.registry = config_table - setattr(new_cls, "_sa_registry", config_registry) - setattr(new_cls, "metadata", config_registry.metadata) - setattr(new_cls, "__abstract__", True) + setattr(new_cls, "_sa_registry", config_registry) # noqa: B010 + setattr(new_cls, "metadata", config_registry.metadata) # noqa: B010 + setattr(new_cls, "__abstract__", True) # noqa: B010 return new_cls # Override SQLAlchemy, allow both SQLAlchemy and plain Pydantic models @@ -320,7 +329,7 @@ def __init__( # triggers an error base_is_table = False for base in bases: - config = getattr(base, "__config__") + config = getattr(base, "__config__") # noqa: B009 if config and getattr(config, "table", False): base_is_table = True break @@ -351,7 +360,7 @@ def __init__( rel_kwargs["back_populates"] = rel_info.back_populates if rel_info.link_model: ins = inspect(rel_info.link_model) - local_table = getattr(ins, "local_table") + local_table = getattr(ins, "local_table") # noqa: B009 if local_table is None: raise RuntimeError( "Couldn't find the secondary table for " @@ -430,7 +439,7 @@ def get_column_from_field(field: ModelField) -> Column: # type: ignore # Override derived nullability if the nullable property is set explicitly # on the field if hasattr(field.field_info, "nullable"): - field_nullable = getattr(field.field_info, "nullable") + field_nullable = getattr(field.field_info, "nullable") # noqa: B009 if field_nullable != Undefined: nullable = field_nullable args = [] From 7f72c60ae44dc20e42e22b322afa08e31d4553ea Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 23 Oct 2023 07:35:25 +0000 Subject: [PATCH 225/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 8ba2f57a69..5e79cf064d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Move to Ruff and add pre-commit. PR [#661](https://github.com/tiangolo/sqlmodel/pull/661) by [@tiangolo](https://github.com/tiangolo). * ✨ Raise a more clear error when a type is not valid. PR [#425](https://github.com/tiangolo/sqlmodel/pull/425) by [@ddanier](https://github.com/ddanier). * 📝 Update outdated link in `docs/db-to-code.md`. PR [#649](https://github.com/tiangolo/sqlmodel/pull/649) by [@MatveyF](https://github.com/MatveyF). * ✏️ Fix typos found with codespell. PR [#520](https://github.com/tiangolo/sqlmodel/pull/520) by [@kianmeng](https://github.com/kianmeng). From 27a81b2112d7cd160c2f2de38430e91ae9ca6158 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 23 Oct 2023 11:46:31 +0400 Subject: [PATCH 226/906] =?UTF-8?q?=F0=9F=8E=A8=20Run=20pre-commit=20on=20?= =?UTF-8?q?all=20files=20and=20autoformat=20(#666)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment-docs-preview-in-pr/app/main.py | 4 +- docs/img/databases/external-server.drawio | 2 +- docs/img/databases/external-server.svg | 2 +- docs/img/databases/multiple-servers.drawio | 2 +- docs/img/databases/multiple-servers.svg | 2 +- docs/img/databases/relationships.drawio | 2 +- docs/img/databases/relationships.svg | 2 +- docs/img/databases/same-server.drawio | 2 +- docs/img/databases/same-server.svg | 2 +- docs/img/databases/single-file.drawio | 2 +- docs/img/databases/single-file.svg | 2 +- docs/img/db-to-code/mapper.drawio | 2 +- docs/img/db-to-code/mapper.svg | 2 +- .../img/tutorial/indexes/dictionary001.drawio | 2 +- docs/img/tutorial/indexes/dictionary001.svg | 2 +- .../img/tutorial/indexes/dictionary002.drawio | 2 +- docs/img/tutorial/indexes/dictionary002.svg | 2 +- .../img/tutorial/indexes/dictionary003.drawio | 2 +- docs/img/tutorial/indexes/dictionary003.svg | 2 +- .../img/tutorial/indexes/dictionary004.drawio | 2 +- docs/img/tutorial/indexes/dictionary004.svg | 2 +- .../img/tutorial/indexes/dictionary005.drawio | 2 +- docs/img/tutorial/indexes/dictionary005.svg | 2 +- .../img/tutorial/indexes/dictionary006.drawio | 2 +- docs/img/tutorial/indexes/dictionary006.svg | 2 +- .../img/tutorial/indexes/dictionary007.drawio | 2 +- docs/img/tutorial/indexes/dictionary007.svg | 2 +- .../img/tutorial/indexes/dictionary008.drawio | 2 +- docs/img/tutorial/indexes/dictionary008.svg | 2 +- docs/img/tutorial/indexes/techbook001.drawio | 2 +- docs/img/tutorial/indexes/techbook001.svg | 2 +- .../tutorial/many-to-many/many-to-many.drawio | 2 +- .../tutorial/many-to-many/many-to-many.svg | 2 +- .../tutorial/offset-and-limit/limit.drawio | 2 +- docs/img/tutorial/offset-and-limit/limit.svg | 2 +- .../tutorial/offset-and-limit/limit2.drawio | 2 +- docs/img/tutorial/offset-and-limit/limit2.svg | 2 +- .../tutorial/offset-and-limit/limit3.drawio | 2 +- docs/img/tutorial/offset-and-limit/limit3.svg | 2 +- .../attributes/back-populates.drawio | 2 +- .../attributes/back-populates.svg | 2 +- .../attributes/back-populates2.drawio | 2 +- .../attributes/back-populates2.svg | 2 +- .../select/relationships2.drawio | 2 +- .../relationships/select/relationships2.svg | 2 +- docs/js/termynal.js | 14 ++-- docs/overrides/main.html | 8 +- docs/tutorial/automatic-id-none-refresh.md | 76 +++++++++--------- .../tutorial/connect/create-connected-rows.md | 22 +++--- .../connect/create-connected-tables.md | 40 +++++----- docs/tutorial/connect/read-connected-data.md | 16 ++-- .../connect/remove-data-connections.md | 6 +- .../connect/update-data-connections.md | 6 +- docs/tutorial/create-db-and-table.md | 10 +-- docs/tutorial/delete.md | 8 +- docs/tutorial/index.md | 2 +- docs/tutorial/indexes.md | 12 +-- docs/tutorial/limit-and-offset.md | 12 +-- docs/tutorial/many-to-many/create-data.md | 24 +++--- .../many-to-many/create-models-with-link.md | 28 +++---- .../many-to-many/link-with-extra-fields.md | 78 +++++++++---------- .../update-remove-relationships.md | 28 +++---- docs/tutorial/one.md | 28 +++---- .../relationship-attributes/back-populates.md | 24 +++--- .../read-relationships.md | 4 +- docs/tutorial/select.md | 4 +- docs/tutorial/update.md | 8 +- docs/tutorial/where.md | 16 ++-- .../annotations/en/tutorial002.md | 24 +++--- .../delete/annotations/en/tutorial002.md | 8 +- .../select/annotations/en/tutorial002.md | 2 +- .../update/annotations/en/tutorial002.md | 8 +- .../update/annotations/en/tutorial004.md | 16 ++-- sqlmodel/sql/sqltypes.py | 1 - .../test_delete/test_tutorial001.py | 1 - .../test_limit_and_offset/test_tutorial001.py | 1 - .../test_multiple_models/test_tutorial001.py | 1 - .../test_multiple_models/test_tutorial002.py | 1 - .../test_read_one/test_tutorial001.py | 1 - .../test_response_model/test_tutorial001.py | 1 - .../test_tutorial001.py | 1 - .../test_simple_hero_api/test_tutorial001.py | 1 - .../test_teams/test_tutorial001.py | 1 - .../test_update/test_tutorial001.py | 1 - 84 files changed, 311 insertions(+), 324 deletions(-) diff --git a/.github/actions/comment-docs-preview-in-pr/app/main.py b/.github/actions/comment-docs-preview-in-pr/app/main.py index 3b10e0ee08..c9fb7cbbef 100644 --- a/.github/actions/comment-docs-preview-in-pr/app/main.py +++ b/.github/actions/comment-docs-preview-in-pr/app/main.py @@ -48,9 +48,7 @@ class PartialGithubEvent(BaseModel): use_pr = pr break if not use_pr: - logging.error( - f"No PR found for hash: {event.workflow_run.head_commit.id}" - ) + logging.error(f"No PR found for hash: {event.workflow_run.head_commit.id}") sys.exit(0) github_headers = { "Authorization": f"token {settings.input_token.get_secret_value()}" diff --git a/docs/img/databases/external-server.drawio b/docs/img/databases/external-server.drawio index 4af8e30d6c..7631d02662 100644 --- a/docs/img/databases/external-server.drawio +++ b/docs/img/databases/external-server.drawio @@ -90,4 +90,4 @@ - \ No newline at end of file + diff --git a/docs/img/databases/external-server.svg b/docs/img/databases/external-server.svg index cf0f541bfa..4a85c58225 100644 --- a/docs/img/databases/external-server.svg +++ b/docs/img/databases/external-server.svg @@ -1 +1 @@ -
Machine / Computer
Machine / Computer
Database application
Database application
File
File
Data
Data
File
File
Data
Data
File
File
Data
Data
Machine / Computer
Machine / Computer
Your code
Your code
Viewer does not support full SVG 1.1
\ No newline at end of file +
Machine / Computer
Machine / Computer
Database application
Database application
File
File
Data
Data
File
File
Data
Data
File
File
Data
Data
Machine / Computer
Machine / Computer
Your code
Your code
Viewer does not support full SVG 1.1
diff --git a/docs/img/databases/multiple-servers.drawio b/docs/img/databases/multiple-servers.drawio index efceed9c08..9a4fd542f2 100644 --- a/docs/img/databases/multiple-servers.drawio +++ b/docs/img/databases/multiple-servers.drawio @@ -202,4 +202,4 @@ - \ No newline at end of file + diff --git a/docs/img/databases/multiple-servers.svg b/docs/img/databases/multiple-servers.svg index 039260a608..083d0e73bd 100644 --- a/docs/img/databases/multiple-servers.svg +++ b/docs/img/databases/multiple-servers.svg @@ -1 +1 @@ -
Machine / Computer
Machine / Computer
Database application
Database application
File
File
Data
Data
File
File
Data
Data
Machine / Computer
Machine / Computer
Your code
Your code
Machine / Computer
Machine / Computer
Database application
Database application
File
File
Data
Data
File
File
Data
Data
Machine / Computer
Machine / Computer
Database application
Database application
File
File
Data
Data
File
File
Data
Data
Viewer does not support full SVG 1.1
\ No newline at end of file +
Machine / Computer
Machine / Computer
Database application
Database application
File
File
Data
Data
File
File
Data
Data
Machine / Computer
Machine / Computer
Your code
Your code
Machine / Computer
Machine / Computer
Database application
Database application
File
File
Data
Data
File
File
Data
Data
Machine / Computer
Machine / Computer
Database application
Database application
File
File
Data
Data
File
File
Data
Data
Viewer does not support full SVG 1.1
diff --git a/docs/img/databases/relationships.drawio b/docs/img/databases/relationships.drawio index 9ae668cf99..fb8ef8d128 100644 --- a/docs/img/databases/relationships.drawio +++ b/docs/img/databases/relationships.drawio @@ -148,4 +148,4 @@ - \ No newline at end of file + diff --git a/docs/img/databases/relationships.svg b/docs/img/databases/relationships.svg index fbecdac05a..055727579f 100644 --- a/docs/img/databases/relationships.svg +++ b/docs/img/databases/relationships.svg @@ -54,4 +54,4 @@ src: url("https://fonts.gstatic.com/s/roboto/v27/KFOmCnqEu92Fr1Mu4mxK.woff2") format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } -
hero
hero
id
id
name
name
secret_name
secret_name
age
age
team_id
team_id
1
1
Deadpond
Deadpond
Dive Wilson
Dive Wilson
null
null
2
2
2
2
Spider-Boy
Spider-Boy
Pedro Parqueador
Pedro Parqueador
null
null
1
1
3
3
Rusty-Man
Rusty-Man
Tommy Sharp
Tommy Sharp
48
48
1
1
team
team
id
id
name
name
headquarters
headquarters
1
1
Preventers
Preventers
Sharp Tower
Sharp Tower
2
2
Z-Force
Z-Force

Sister Margaret’s Bar

Sister Margaret’s Bar
Viewer does not support full SVG 1.1 \ No newline at end of file +
hero
hero
id
id
name
name
secret_name
secret_name
age
age
team_id
team_id
1
1
Deadpond
Deadpond
Dive Wilson
Dive Wilson
null
null
2
2
2
2
Spider-Boy
Spider-Boy
Pedro Parqueador
Pedro Parqueador
null
null
1
1
3
3
Rusty-Man
Rusty-Man
Tommy Sharp
Tommy Sharp
48
48
1
1
team
team
id
id
name
name
headquarters
headquarters
1
1
Preventers
Preventers
Sharp Tower
Sharp Tower
2
2
Z-Force
Z-Force

Sister Margaret’s Bar

Sister Margaret’s Bar
Viewer does not support full SVG 1.1 diff --git a/docs/img/databases/same-server.drawio b/docs/img/databases/same-server.drawio index 596c64956c..4f43be43cb 100644 --- a/docs/img/databases/same-server.drawio +++ b/docs/img/databases/same-server.drawio @@ -81,4 +81,4 @@ - \ No newline at end of file + diff --git a/docs/img/databases/same-server.svg b/docs/img/databases/same-server.svg index 7f2a77b73c..a3f6dff36b 100644 --- a/docs/img/databases/same-server.svg +++ b/docs/img/databases/same-server.svg @@ -1 +1 @@ -
Machine / Computer
Machine / Computer
Database application
Database application
File
File
Data
Data
File
File
Data
Data
File
File
Data
Data
Your code
Your code
Viewer does not support full SVG 1.1
\ No newline at end of file +
Machine / Computer
Machine / Computer
Database application
Database application
File
File
Data
Data
File
File
Data
Data
File
File
Data
Data
Your code
Your code
Viewer does not support full SVG 1.1
diff --git a/docs/img/databases/single-file.drawio b/docs/img/databases/single-file.drawio index 52ce703c4c..c379f71c1c 100644 --- a/docs/img/databases/single-file.drawio +++ b/docs/img/databases/single-file.drawio @@ -34,4 +34,4 @@ - \ No newline at end of file + diff --git a/docs/img/databases/single-file.svg b/docs/img/databases/single-file.svg index f2526d952f..52c91e573d 100644 --- a/docs/img/databases/single-file.svg +++ b/docs/img/databases/single-file.svg @@ -1 +1 @@ -
Machine / Computer
Machine / Computer
Your code
Your code
File: heroes.db
File: heroes.db
Data
Data
Viewer does not support full SVG 1.1
\ No newline at end of file +
Machine / Computer
Machine / Computer
Your code
Your code
File: heroes.db
File: heroes.db
Data
Data
Viewer does not support full SVG 1.1
diff --git a/docs/img/db-to-code/mapper.drawio b/docs/img/db-to-code/mapper.drawio index 072b629b54..3b002eb3f8 100644 --- a/docs/img/db-to-code/mapper.drawio +++ b/docs/img/db-to-code/mapper.drawio @@ -61,4 +61,4 @@ - \ No newline at end of file + diff --git a/docs/img/db-to-code/mapper.svg b/docs/img/db-to-code/mapper.svg index 496c907464..e31a464bba 100644 --- a/docs/img/db-to-code/mapper.svg +++ b/docs/img/db-to-code/mapper.svg @@ -1 +1 @@ -
Set of triangles
Set of triangles
Set of squares
Set of squares
Squares to Triangles Mapper
Squares to Triangles Mapp...
Viewer does not support full SVG 1.1
\ No newline at end of file +
Set of triangles
Set of triangles
Set of squares
Set of squares
Squares to Triangles Mapper
Squares to Triangles Mapp...
Viewer does not support full SVG 1.1
diff --git a/docs/img/tutorial/indexes/dictionary001.drawio b/docs/img/tutorial/indexes/dictionary001.drawio index 659f6b52a4..84992d0213 100644 --- a/docs/img/tutorial/indexes/dictionary001.drawio +++ b/docs/img/tutorial/indexes/dictionary001.drawio @@ -94,4 +94,4 @@ - \ No newline at end of file + diff --git a/docs/img/tutorial/indexes/dictionary001.svg b/docs/img/tutorial/indexes/dictionary001.svg index b543793a25..59fc39294e 100644 --- a/docs/img/tutorial/indexes/dictionary001.svg +++ b/docs/img/tutorial/indexes/dictionary001.svg @@ -54,4 +54,4 @@ src: url("https://fonts.gstatic.com/s/roboto/v29/KFOmCnqEu92Fr1Mu4mxK.woff2") format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } -
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
M
M
Viewer does not support full SVG 1.1 \ No newline at end of file +
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
M
M
Viewer does not support full SVG 1.1 diff --git a/docs/img/tutorial/indexes/dictionary002.drawio b/docs/img/tutorial/indexes/dictionary002.drawio index cb1857b1ad..52544abde9 100644 --- a/docs/img/tutorial/indexes/dictionary002.drawio +++ b/docs/img/tutorial/indexes/dictionary002.drawio @@ -94,4 +94,4 @@ - \ No newline at end of file + diff --git a/docs/img/tutorial/indexes/dictionary002.svg b/docs/img/tutorial/indexes/dictionary002.svg index 677687d248..d612925125 100644 --- a/docs/img/tutorial/indexes/dictionary002.svg +++ b/docs/img/tutorial/indexes/dictionary002.svg @@ -1 +1 @@ -
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
M
M
Viewer does not support full SVG 1.1
\ No newline at end of file +
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
M
M
Viewer does not support full SVG 1.1
diff --git a/docs/img/tutorial/indexes/dictionary003.drawio b/docs/img/tutorial/indexes/dictionary003.drawio index 845eb065cd..d37353364c 100644 --- a/docs/img/tutorial/indexes/dictionary003.drawio +++ b/docs/img/tutorial/indexes/dictionary003.drawio @@ -94,4 +94,4 @@ - \ No newline at end of file + diff --git a/docs/img/tutorial/indexes/dictionary003.svg b/docs/img/tutorial/indexes/dictionary003.svg index d667a68893..0eafd5c561 100644 --- a/docs/img/tutorial/indexes/dictionary003.svg +++ b/docs/img/tutorial/indexes/dictionary003.svg @@ -1 +1 @@ -
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
Viewer does not support full SVG 1.1
\ No newline at end of file +
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
Viewer does not support full SVG 1.1
diff --git a/docs/img/tutorial/indexes/dictionary004.drawio b/docs/img/tutorial/indexes/dictionary004.drawio index 14bbb1e26e..6c8590d6ea 100644 --- a/docs/img/tutorial/indexes/dictionary004.drawio +++ b/docs/img/tutorial/indexes/dictionary004.drawio @@ -97,4 +97,4 @@ - \ No newline at end of file + diff --git a/docs/img/tutorial/indexes/dictionary004.svg b/docs/img/tutorial/indexes/dictionary004.svg index f881d6c9c2..dcfcfc5a59 100644 --- a/docs/img/tutorial/indexes/dictionary004.svg +++ b/docs/img/tutorial/indexes/dictionary004.svg @@ -1 +1 @@ -
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
F
F
Viewer does not support full SVG 1.1
\ No newline at end of file +
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
F
F
Viewer does not support full SVG 1.1
diff --git a/docs/img/tutorial/indexes/dictionary005.drawio b/docs/img/tutorial/indexes/dictionary005.drawio index 9e339c177e..33e21c6d90 100644 --- a/docs/img/tutorial/indexes/dictionary005.drawio +++ b/docs/img/tutorial/indexes/dictionary005.drawio @@ -94,4 +94,4 @@ - \ No newline at end of file + diff --git a/docs/img/tutorial/indexes/dictionary005.svg b/docs/img/tutorial/indexes/dictionary005.svg index 9d376245c0..4bfa8c0ca4 100644 --- a/docs/img/tutorial/indexes/dictionary005.svg +++ b/docs/img/tutorial/indexes/dictionary005.svg @@ -1 +1 @@ -
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
Viewer does not support full SVG 1.1
\ No newline at end of file +
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
Viewer does not support full SVG 1.1
diff --git a/docs/img/tutorial/indexes/dictionary006.drawio b/docs/img/tutorial/indexes/dictionary006.drawio index 3c669d323f..84366e87b0 100644 --- a/docs/img/tutorial/indexes/dictionary006.drawio +++ b/docs/img/tutorial/indexes/dictionary006.drawio @@ -97,4 +97,4 @@ - \ No newline at end of file + diff --git a/docs/img/tutorial/indexes/dictionary006.svg b/docs/img/tutorial/indexes/dictionary006.svg index 30be80ea8b..f893ca4cd4 100644 --- a/docs/img/tutorial/indexes/dictionary006.svg +++ b/docs/img/tutorial/indexes/dictionary006.svg @@ -1 +1 @@ -
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
C
C
Viewer does not support full SVG 1.1
\ No newline at end of file +
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
C
C
Viewer does not support full SVG 1.1
diff --git a/docs/img/tutorial/indexes/dictionary007.drawio b/docs/img/tutorial/indexes/dictionary007.drawio index 89b32cabaf..62ad844200 100644 --- a/docs/img/tutorial/indexes/dictionary007.drawio +++ b/docs/img/tutorial/indexes/dictionary007.drawio @@ -97,4 +97,4 @@ - \ No newline at end of file + diff --git a/docs/img/tutorial/indexes/dictionary007.svg b/docs/img/tutorial/indexes/dictionary007.svg index 79e795060e..8f71aa8311 100644 --- a/docs/img/tutorial/indexes/dictionary007.svg +++ b/docs/img/tutorial/indexes/dictionary007.svg @@ -1 +1 @@ -
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
Viewer does not support full SVG 1.1
\ No newline at end of file +
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
Viewer does not support full SVG 1.1
diff --git a/docs/img/tutorial/indexes/dictionary008.drawio b/docs/img/tutorial/indexes/dictionary008.drawio index ac08ad04d4..30a1f66277 100644 --- a/docs/img/tutorial/indexes/dictionary008.drawio +++ b/docs/img/tutorial/indexes/dictionary008.drawio @@ -100,4 +100,4 @@ - \ No newline at end of file + diff --git a/docs/img/tutorial/indexes/dictionary008.svg b/docs/img/tutorial/indexes/dictionary008.svg index 013a4d64a3..5a48e18a20 100644 --- a/docs/img/tutorial/indexes/dictionary008.svg +++ b/docs/img/tutorial/indexes/dictionary008.svg @@ -1 +1 @@ -
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
D
D
Viewer does not support full SVG 1.1
\ No newline at end of file +
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
D
D
Viewer does not support full SVG 1.1
diff --git a/docs/img/tutorial/indexes/techbook001.drawio b/docs/img/tutorial/indexes/techbook001.drawio index de1c25668c..5fffe3c3ea 100644 --- a/docs/img/tutorial/indexes/techbook001.drawio +++ b/docs/img/tutorial/indexes/techbook001.drawio @@ -89,4 +89,4 @@ - \ No newline at end of file + diff --git a/docs/img/tutorial/indexes/techbook001.svg b/docs/img/tutorial/indexes/techbook001.svg index 8b0c09ddcf..3f44f50742 100644 --- a/docs/img/tutorial/indexes/techbook001.svg +++ b/docs/img/tutorial/indexes/techbook001.svg @@ -1 +1 @@ -
Technical Book
Technical Book
Chapter 1
Chapter 1
Chapter 2
Chapter 2
Chapter 3
Chapter 3
Chapter 4
Chapter 4
Chapter 5
Chapter 5
Chapter 6
Chapter 6
Chapter 7
Chapter 7
Book Index
Book Index
Database
Database
Python
Python
Files
Files
Editors
Editors
Viewer does not support full SVG 1.1
\ No newline at end of file +
Technical Book
Technical Book
Chapter 1
Chapter 1
Chapter 2
Chapter 2
Chapter 3
Chapter 3
Chapter 4
Chapter 4
Chapter 5
Chapter 5
Chapter 6
Chapter 6
Chapter 7
Chapter 7
Book Index
Book Index
Database
Database
Python
Python
Files
Files
Editors
Editors
Viewer does not support full SVG 1.1
diff --git a/docs/img/tutorial/many-to-many/many-to-many.drawio b/docs/img/tutorial/many-to-many/many-to-many.drawio index d585e5e0e6..bbe1176076 100644 --- a/docs/img/tutorial/many-to-many/many-to-many.drawio +++ b/docs/img/tutorial/many-to-many/many-to-many.drawio @@ -217,4 +217,4 @@ - \ No newline at end of file + diff --git a/docs/img/tutorial/many-to-many/many-to-many.svg b/docs/img/tutorial/many-to-many/many-to-many.svg index d35c280352..aa0c6cc457 100644 --- a/docs/img/tutorial/many-to-many/many-to-many.svg +++ b/docs/img/tutorial/many-to-many/many-to-many.svg @@ -54,4 +54,4 @@ src: url("https://fonts.gstatic.com/s/roboto/v27/KFOmCnqEu92Fr1Mu4mxK.woff2") format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } -
hero
hero
id
id
name
name
secret_name
secret_name
age
age
1
1
Deadpond
Deadpond
Dive Wilson
Dive Wilson
null
null
2
2
Spider-Boy
Spider-Boy
Pedro Parqueador
Pedro Parqueador
null
null
3
3
Rusty-Man
Rusty-Man
Tommy Sharp
Tommy Sharp
48
48
team
team
id
id
name
name
headquarters
headquarters
1
1
Preventers
Preventers
Sharp Tower
Sharp Tower
2
2
Z-Force
Z-Force

Sister Margaret’s Bar

Sister Margaret’s Bar
heroteamlink
heroteamlink
hero_id
hero_id
team_id
team_id
1
1
1
1
1
1
2
2
2
2
1
1
3
3
1
1
Viewer does not support full SVG 1.1 \ No newline at end of file +
hero
hero
id
id
name
name
secret_name
secret_name
age
age
1
1
Deadpond
Deadpond
Dive Wilson
Dive Wilson
null
null
2
2
Spider-Boy
Spider-Boy
Pedro Parqueador
Pedro Parqueador
null
null
3
3
Rusty-Man
Rusty-Man
Tommy Sharp
Tommy Sharp
48
48
team
team
id
id
name
name
headquarters
headquarters
1
1
Preventers
Preventers
Sharp Tower
Sharp Tower
2
2
Z-Force
Z-Force

Sister Margaret’s Bar

Sister Margaret’s Bar
heroteamlink
heroteamlink
hero_id
hero_id
team_id
team_id
1
1
1
1
1
1
2
2
2
2
1
1
3
3
1
1
Viewer does not support full SVG 1.1 diff --git a/docs/img/tutorial/offset-and-limit/limit.drawio b/docs/img/tutorial/offset-and-limit/limit.drawio index 569dfc5437..da7f17ce99 100644 --- a/docs/img/tutorial/offset-and-limit/limit.drawio +++ b/docs/img/tutorial/offset-and-limit/limit.drawio @@ -130,4 +130,4 @@ - \ No newline at end of file + diff --git a/docs/img/tutorial/offset-and-limit/limit.svg b/docs/img/tutorial/offset-and-limit/limit.svg index 3e31086a10..d05669e3c5 100644 --- a/docs/img/tutorial/offset-and-limit/limit.svg +++ b/docs/img/tutorial/offset-and-limit/limit.svg @@ -54,4 +54,4 @@ src: url("https://fonts.gstatic.com/s/roboto/v27/KFOmCnqEu92Fr1Mu4mxK.woff2") format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } -
hero
hero
id
id
name
name
secret_name
secret_name
age
age
1
1
Deadpond
Deadpond
Dive Wilson
Dive Wilson
null
null
2
2
Spider-Boy
Spider-Boy
Pedro Parqueador
Pedro Parqueador
null
null
3
3
Rusty-Man
Rusty-Man
Tommy Sharp
Tommy Sharp
48
48
4
4
Tarantula
Tarantula
Natalia Roman-on
Natalia Roman-on
32
32
5
5
Black Lion
Black Lion
Trevor Challa
Trevor Challa
35
35
6
6
Dr. Weird
Dr. Weird
Steve Weird
Steve Weird
36
36
7
7
Captain North America
Captain North America
Esteban Rogelios
Esteban Rogelios
93
93
Viewer does not support full SVG 1.1 \ No newline at end of file +
hero
hero
id
id
name
name
secret_name
secret_name
age
age
1
1
Deadpond
Deadpond
Dive Wilson
Dive Wilson
null
null
2
2
Spider-Boy
Spider-Boy
Pedro Parqueador
Pedro Parqueador
null
null
3
3
Rusty-Man
Rusty-Man
Tommy Sharp
Tommy Sharp
48
48
4
4
Tarantula
Tarantula
Natalia Roman-on
Natalia Roman-on
32
32
5
5
Black Lion
Black Lion
Trevor Challa
Trevor Challa
35
35
6
6
Dr. Weird
Dr. Weird
Steve Weird
Steve Weird
36
36
7
7
Captain North America
Captain North America
Esteban Rogelios
Esteban Rogelios
93
93
Viewer does not support full SVG 1.1 diff --git a/docs/img/tutorial/offset-and-limit/limit2.drawio b/docs/img/tutorial/offset-and-limit/limit2.drawio index a329235f71..68f9817052 100644 --- a/docs/img/tutorial/offset-and-limit/limit2.drawio +++ b/docs/img/tutorial/offset-and-limit/limit2.drawio @@ -130,4 +130,4 @@ - \ No newline at end of file + diff --git a/docs/img/tutorial/offset-and-limit/limit2.svg b/docs/img/tutorial/offset-and-limit/limit2.svg index a3a7e81aa7..f574f13640 100644 --- a/docs/img/tutorial/offset-and-limit/limit2.svg +++ b/docs/img/tutorial/offset-and-limit/limit2.svg @@ -54,4 +54,4 @@ src: url("https://fonts.gstatic.com/s/roboto/v27/KFOmCnqEu92Fr1Mu4mxK.woff2") format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } -
hero
hero
id
id
name
name
secret_name
secret_name
age
age
1
1
Deadpond
Deadpond
Dive Wilson
Dive Wilson
null
null
2
2
Spider-Boy
Spider-Boy
Pedro Parqueador
Pedro Parqueador
null
null
3
3
Rusty-Man
Rusty-Man
Tommy Sharp
Tommy Sharp
48
48
4
4
Tarantula
Tarantula
Natalia Roman-on
Natalia Roman-on
32
32
5
5
Black Lion
Black Lion
Trevor Challa
Trevor Challa
35
35
6
6
Dr. Weird
Dr. Weird
Steve Weird
Steve Weird
36
36
7
7
Captain North America
Captain North America
Esteban Rogelios
Esteban Rogelios
93
93
Viewer does not support full SVG 1.1 \ No newline at end of file +
hero
hero
id
id
name
name
secret_name
secret_name
age
age
1
1
Deadpond
Deadpond
Dive Wilson
Dive Wilson
null
null
2
2
Spider-Boy
Spider-Boy
Pedro Parqueador
Pedro Parqueador
null
null
3
3
Rusty-Man
Rusty-Man
Tommy Sharp
Tommy Sharp
48
48
4
4
Tarantula
Tarantula
Natalia Roman-on
Natalia Roman-on
32
32
5
5
Black Lion
Black Lion
Trevor Challa
Trevor Challa
35
35
6
6
Dr. Weird
Dr. Weird
Steve Weird
Steve Weird
36
36
7
7
Captain North America
Captain North America
Esteban Rogelios
Esteban Rogelios
93
93
Viewer does not support full SVG 1.1 diff --git a/docs/img/tutorial/offset-and-limit/limit3.drawio b/docs/img/tutorial/offset-and-limit/limit3.drawio index 2b2b20db2c..93e4c2df53 100644 --- a/docs/img/tutorial/offset-and-limit/limit3.drawio +++ b/docs/img/tutorial/offset-and-limit/limit3.drawio @@ -130,4 +130,4 @@ - \ No newline at end of file + diff --git a/docs/img/tutorial/offset-and-limit/limit3.svg b/docs/img/tutorial/offset-and-limit/limit3.svg index 5858359964..8a1a560b00 100644 --- a/docs/img/tutorial/offset-and-limit/limit3.svg +++ b/docs/img/tutorial/offset-and-limit/limit3.svg @@ -54,4 +54,4 @@ src: url("https://fonts.gstatic.com/s/roboto/v27/KFOmCnqEu92Fr1Mu4mxK.woff2") format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } -
hero
hero
id
id
name
name
secret_name
secret_name
age
age
1
1
Deadpond
Deadpond
Dive Wilson
Dive Wilson
null
null
2
2
Spider-Boy
Spider-Boy
Pedro Parqueador
Pedro Parqueador
null
null
3
3
Rusty-Man
Rusty-Man
Tommy Sharp
Tommy Sharp
48
48
4
4
Tarantula
Tarantula
Natalia Roman-on
Natalia Roman-on
32
32
5
5
Black Lion
Black Lion
Trevor Challa
Trevor Challa
35
35
6
6
Dr. Weird
Dr. Weird
Steve Weird
Steve Weird
36
36
7
7
Captain North America
Captain North America
Esteban Rogelios
Esteban Rogelios
93
93
Viewer does not support full SVG 1.1 \ No newline at end of file +
hero
hero
id
id
name
name
secret_name
secret_name
age
age
1
1
Deadpond
Deadpond
Dive Wilson
Dive Wilson
null
null
2
2
Spider-Boy
Spider-Boy
Pedro Parqueador
Pedro Parqueador
null
null
3
3
Rusty-Man
Rusty-Man
Tommy Sharp
Tommy Sharp
48
48
4
4
Tarantula
Tarantula
Natalia Roman-on
Natalia Roman-on
32
32
5
5
Black Lion
Black Lion
Trevor Challa
Trevor Challa
35
35
6
6
Dr. Weird
Dr. Weird
Steve Weird
Steve Weird
36
36
7
7
Captain North America
Captain North America
Esteban Rogelios
Esteban Rogelios
93
93
Viewer does not support full SVG 1.1 diff --git a/docs/img/tutorial/relationships/attributes/back-populates.drawio b/docs/img/tutorial/relationships/attributes/back-populates.drawio index 5ef9c03264..5fc8c6fa73 100644 --- a/docs/img/tutorial/relationships/attributes/back-populates.drawio +++ b/docs/img/tutorial/relationships/attributes/back-populates.drawio @@ -38,4 +38,4 @@ - \ No newline at end of file + diff --git a/docs/img/tutorial/relationships/attributes/back-populates.svg b/docs/img/tutorial/relationships/attributes/back-populates.svg index dabf36de1f..631f73c808 100644 --- a/docs/img/tutorial/relationships/attributes/back-populates.svg +++ b/docs/img/tutorial/relationships/attributes/back-populates.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/docs/img/tutorial/relationships/attributes/back-populates2.drawio b/docs/img/tutorial/relationships/attributes/back-populates2.drawio index 7313c464e9..c5d283feb0 100644 --- a/docs/img/tutorial/relationships/attributes/back-populates2.drawio +++ b/docs/img/tutorial/relationships/attributes/back-populates2.drawio @@ -49,4 +49,4 @@ - \ No newline at end of file + diff --git a/docs/img/tutorial/relationships/attributes/back-populates2.svg b/docs/img/tutorial/relationships/attributes/back-populates2.svg index 83ed8154a6..929cb0a042 100644 --- a/docs/img/tutorial/relationships/attributes/back-populates2.svg +++ b/docs/img/tutorial/relationships/attributes/back-populates2.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/docs/img/tutorial/relationships/select/relationships2.drawio b/docs/img/tutorial/relationships/select/relationships2.drawio index 0086af56c4..afd77f4e9e 100644 --- a/docs/img/tutorial/relationships/select/relationships2.drawio +++ b/docs/img/tutorial/relationships/select/relationships2.drawio @@ -139,4 +139,4 @@ - \ No newline at end of file + diff --git a/docs/img/tutorial/relationships/select/relationships2.svg b/docs/img/tutorial/relationships/select/relationships2.svg index e2f987eba7..c0987c7c32 100644 --- a/docs/img/tutorial/relationships/select/relationships2.svg +++ b/docs/img/tutorial/relationships/select/relationships2.svg @@ -54,4 +54,4 @@ src: url("https://fonts.gstatic.com/s/roboto/v27/KFOmCnqEu92Fr1Mu4mxK.woff2") format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } -
hero
hero
id
id
name
name
secret_name
secret_name
age
age
team_id
team_id
1
1
Deadpond
Deadpond
Dive Wilson
Dive Wilson
null
null
2
2
2
2
Spider-Boy
Spider-Boy
Pedro Parqueador
Pedro Parqueador
null
null
null
null
3
3
Rusty-Man
Rusty-Man
Tommy Sharp
Tommy Sharp
48
48
1
1
team
team
id
id
name
name
headquarters
headquarters
1
1
Preventers
Preventers
Sharp Tower
Sharp Tower
2
2
Z-Force
Z-Force

Sister Margaret’s Bar

Sister Margaret’s Bar
Viewer does not support full SVG 1.1 \ No newline at end of file +
hero
hero
id
id
name
name
secret_name
secret_name
age
age
team_id
team_id
1
1
Deadpond
Deadpond
Dive Wilson
Dive Wilson
null
null
2
2
2
2
Spider-Boy
Spider-Boy
Pedro Parqueador
Pedro Parqueador
null
null
null
null
3
3
Rusty-Man
Rusty-Man
Tommy Sharp
Tommy Sharp
48
48
1
1
team
team
id
id
name
name
headquarters
headquarters
1
1
Preventers
Preventers
Sharp Tower
Sharp Tower
2
2
Z-Force
Z-Force

Sister Margaret’s Bar

Sister Margaret’s Bar
Viewer does not support full SVG 1.1 diff --git a/docs/js/termynal.js b/docs/js/termynal.js index c21e437501..45bb371c83 100644 --- a/docs/js/termynal.js +++ b/docs/js/termynal.js @@ -72,14 +72,14 @@ class Termynal { * Initialise the widget, get lines, clear container and start animation. */ init() { - /** + /** * Calculates width and height of Termynal container. * If container is empty and lines are dynamically loaded, defaults to browser `auto` or CSS. - */ + */ const containerStyle = getComputedStyle(this.container); - this.container.style.width = containerStyle.width !== '0px' ? + this.container.style.width = containerStyle.width !== '0px' ? containerStyle.width : undefined; - this.container.style.minHeight = containerStyle.height !== '0px' ? + this.container.style.minHeight = containerStyle.height !== '0px' ? containerStyle.height : undefined; this.container.setAttribute('data-termynal', ''); @@ -138,7 +138,7 @@ class Termynal { restart.innerHTML = "restart ↻" return restart } - + generateFinish() { const finish = document.createElement('a') finish.onclick = (e) => { @@ -215,7 +215,7 @@ class Termynal { /** * Converts line data objects into line elements. - * + * * @param {Object[]} lineData - Dynamically loaded lines. * @param {Object} line - Line data object. * @returns {Element[]} - Array of line elements. @@ -231,7 +231,7 @@ class Termynal { /** * Helper function for generating attributes string. - * + * * @param {Object} line - Line data object. * @returns {string} - String of attributes. */ diff --git a/docs/overrides/main.html b/docs/overrides/main.html index fc5bce873f..7440b084ad 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -12,9 +12,9 @@ }); }); - -{%- endblock %} \ No newline at end of file +{%- endblock %} diff --git a/docs/tutorial/automatic-id-none-refresh.md b/docs/tutorial/automatic-id-none-refresh.md index d41cd14e91..c7cf975ad4 100644 --- a/docs/tutorial/automatic-id-none-refresh.md +++ b/docs/tutorial/automatic-id-none-refresh.md @@ -198,9 +198,9 @@ INFO Engine COMMIT // And now our prints After committing the session -Hero 1: -Hero 2: -Hero 3: +Hero 1: +Hero 2: +Hero 3: // What is happening here? 😱 ``` @@ -275,17 +275,17 @@ $ python app.py // After committing, the objects are expired and have no values After committing the session -Hero 1: -Hero 2: -Hero 3: +Hero 1: +Hero 2: +Hero 3: // Now we will access an attribute like the ID, this is the first print After committing the session, show IDs // Notice that before printing the first ID, the Session makes the Engine go to the database to refresh the data 🤓 INFO Engine BEGIN (implicit) -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero WHERE hero.id = ? INFO Engine [generated in 0.00017s] (1,) @@ -293,8 +293,8 @@ INFO Engine [generated in 0.00017s] (1,) Hero 1 ID: 1 // Before the next print, refresh the data for the second object -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.001245s ago] (2,) @@ -302,8 +302,8 @@ INFO Engine [cached since 0.001245s ago] (2,) Hero 2 ID: 2 // Before the third print, refresh its data -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.002215s ago] (3,) @@ -365,20 +365,20 @@ $ python app.py // Output above omitted 👆 // The first refresh -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.id = ? INFO Engine [generated in 0.00024s] (1,) // The second refresh -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.001487s ago] (2,) // The third refresh -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.002377s ago] (3,) @@ -468,12 +468,12 @@ INFO Engine PRAGMA main.table_info("hero") INFO Engine [raw sql] () INFO Engine PRAGMA temp.table_info("hero") INFO Engine [raw sql] () -INFO Engine +INFO Engine CREATE TABLE hero ( - id INTEGER, - name VARCHAR NOT NULL, - secret_name VARCHAR NOT NULL, - age INTEGER, + id INTEGER, + name VARCHAR NOT NULL, + secret_name VARCHAR NOT NULL, + age INTEGER, PRIMARY KEY (id) ) @@ -497,23 +497,23 @@ INFO Engine INSERT INTO hero (name, secret_name, age) VALUES (?, ?, ?) INFO Engine [cached since 0.001483s ago] ('Rusty-Man', 'Tommy Sharp', 48) INFO Engine COMMIT After committing the session -Hero 1: -Hero 2: -Hero 3: +Hero 1: +Hero 2: +Hero 3: After committing the session, show IDs INFO Engine BEGIN (implicit) -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero WHERE hero.id = ? INFO Engine [generated in 0.00029s] (1,) Hero 1 ID: 1 -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.002132s ago] (2,) Hero 2 ID: 2 -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.003367s ago] (3,) Hero 3 ID: 3 @@ -521,16 +521,16 @@ After committing the session, show names Hero 1 name: Deadpond Hero 2 name: Spider-Boy Hero 3 name: Rusty-Man -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.id = ? INFO Engine [generated in 0.00025s] (1,) -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.001583s ago] (2,) -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.002722s ago] (3,) After refreshing the heroes diff --git a/docs/tutorial/connect/create-connected-rows.md b/docs/tutorial/connect/create-connected-rows.md index 657edbde43..beda24a515 100644 --- a/docs/tutorial/connect/create-connected-rows.md +++ b/docs/tutorial/connect/create-connected-rows.md @@ -159,7 +159,7 @@ As the `Hero` class model now has a field (column, attribute) `team_id`, we can We haven't committed this hero to the database yet, but there are already a couple of things to pay **attention** to. -If the database already had some teams, we wouldn't even know **what is the ID** that is going to be automatically assigned to each team by the database, for example, we couldn't just guess `1` or `2`. +If the database already had some teams, we wouldn't even know **what is the ID** that is going to be automatically assigned to each team by the database, for example, we couldn't just guess `1` or `2`. But once the team is created and committed to the database, we can access the object's `id` field to get that ID. @@ -171,8 +171,8 @@ That line alone would generate an output of: ``` INFO Engine BEGIN (implicit) -INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters -FROM team +INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters +FROM team WHERE team.id = ? INFO Engine [generated in 0.00025s] (2,) ``` @@ -199,8 +199,8 @@ Let's now create two more heroes: When creating `hero_rusty_man`, we are accessing `team_preventers.id`, so that will also trigger a refresh of its data, generating an output of: ``` -INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters -FROM team +INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters +FROM team WHERE team.id = ? INFO Engine [cached since 0.001795s ago] (1,) ``` @@ -256,18 +256,18 @@ $ python app.py INFO Engine BEGIN (implicit) // Refresh the first hero -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id +FROM hero WHERE hero.id = ? INFO Engine [generated in 0.00021s] (1,) // Refresh the second hero -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.001575s ago] (2,) // Refresh the third hero -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.002518s ago] (3,) diff --git a/docs/tutorial/connect/create-connected-tables.md b/docs/tutorial/connect/create-connected-tables.md index 14a310f59d..5a9a420a1b 100644 --- a/docs/tutorial/connect/create-connected-tables.md +++ b/docs/tutorial/connect/create-connected-tables.md @@ -191,24 +191,24 @@ INFO Engine PRAGMA temp.table_info("hero") INFO Engine [raw sql] () // Create the tables -INFO Engine +INFO Engine CREATE TABLE team ( - id INTEGER, - name VARCHAR NOT NULL, - headquarters VARCHAR NOT NULL, + id INTEGER, + name VARCHAR NOT NULL, + headquarters VARCHAR NOT NULL, PRIMARY KEY (id) ) INFO Engine [no key 0.00010s] () -INFO Engine +INFO Engine CREATE TABLE hero ( - id INTEGER, - name VARCHAR NOT NULL, - secret_name VARCHAR NOT NULL, - age INTEGER, - team_id INTEGER, - PRIMARY KEY (id), + id INTEGER, + name VARCHAR NOT NULL, + secret_name VARCHAR NOT NULL, + age INTEGER, + team_id INTEGER, + PRIMARY KEY (id), FOREIGN KEY(team_id) REFERENCES team (id) ) @@ -229,9 +229,9 @@ So, the first SQL could also be written as: ```SQL CREATE TABLE team ( - id INTEGER, - name TEXT NOT NULL, - headquarters TEXT NOT NULL, + id INTEGER, + name TEXT NOT NULL, + headquarters TEXT NOT NULL, PRIMARY KEY (id) ) ``` @@ -240,12 +240,12 @@ And the second table could be written as: ```SQL hl_lines="8" CREATE TABLE hero ( - id INTEGER, - name TEXT NOT NULL, - secret_name TEXT NOT NULL, - age INTEGER, - team_id INTEGER, - PRIMARY KEY (id), + id INTEGER, + name TEXT NOT NULL, + secret_name TEXT NOT NULL, + age INTEGER, + team_id INTEGER, + PRIMARY KEY (id), FOREIGN KEY(team_id) REFERENCES team (id) ) ``` diff --git a/docs/tutorial/connect/read-connected-data.md b/docs/tutorial/connect/read-connected-data.md index 88cd754607..0ef3bccf43 100644 --- a/docs/tutorial/connect/read-connected-data.md +++ b/docs/tutorial/connect/read-connected-data.md @@ -203,8 +203,8 @@ $ python app.py // Previous output omitted 😉 // Get the heroes with their teams -2021-08-09 08:55:50,682 INFO sqlalchemy.engine.Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id, team.id AS id_1, team.name AS name_1, team.headquarters -FROM hero, team +2021-08-09 08:55:50,682 INFO sqlalchemy.engine.Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id, team.id AS id_1, team.name AS name_1, team.headquarters +FROM hero, team WHERE hero.team_id = team.id 2021-08-09 08:55:50,682 INFO sqlalchemy.engine.Engine [no key 0.00015s] () @@ -323,7 +323,7 @@ $ python app.py // Previous output omitted 😉 // Select using a JOIN with automatic ON -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id, team.id AS id_1, team.name AS name_1, team.headquarters +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id, team.id AS id_1, team.name AS name_1, team.headquarters FROM hero JOIN team ON team.id = hero.team_id INFO Engine [no key 0.00032s] () @@ -458,7 +458,7 @@ $ python app.py // Previous output omitted 😉 // SELECT using LEFT OUTER JOIN -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id, team.id AS id_1, team.name AS name_1, team.headquarters +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id, team.id AS id_1, team.name AS name_1, team.headquarters FROM hero LEFT OUTER JOIN team ON team.id = hero.team_id INFO Engine [no key 0.00051s] () @@ -522,9 +522,9 @@ If we run that, it would output: $ python app.py // Select only the hero data -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id // But still join with the team table -FROM hero JOIN team ON team.id = hero.team_id +FROM hero JOIN team ON team.id = hero.team_id // And filter with WHERE to get only the Preventers WHERE team.name = ? INFO Engine [no key 0.00066s] ('Preventers',) @@ -564,9 +564,9 @@ And if we run that, it will output: $ python app.py // Select the hero and the team data -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id, team.id AS id_1, team.name AS name_1, team.headquarters +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id, team.id AS id_1, team.name AS name_1, team.headquarters // Join the hero with the team table -FROM hero JOIN team ON team.id = hero.team_id +FROM hero JOIN team ON team.id = hero.team_id // Filter with WHERE to get only Preventers WHERE team.name = ? INFO Engine [no key 0.00018s] ('Preventers',) diff --git a/docs/tutorial/connect/remove-data-connections.md b/docs/tutorial/connect/remove-data-connections.md index f44559b3d1..940a09f30d 100644 --- a/docs/tutorial/connect/remove-data-connections.md +++ b/docs/tutorial/connect/remove-data-connections.md @@ -56,7 +56,7 @@ We can simply set the `team_id` to `None`, and now it doesn't have a connection # Code above omitted 👆 {!./docs_src/tutorial/connect/delete/tutorial001.py[ln:31-32]!} - + # Previous code here omitted 👈 {!./docs_src/tutorial/connect/delete/tutorial001.py[ln:68-72]!} @@ -94,8 +94,8 @@ INFO Engine COMMIT // Automatically start a new transaction INFO Engine BEGIN (implicit) // Refresh the hero -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.1661s ago] (3,) diff --git a/docs/tutorial/connect/update-data-connections.md b/docs/tutorial/connect/update-data-connections.md index b7c8b0daa1..ccc430dd6e 100644 --- a/docs/tutorial/connect/update-data-connections.md +++ b/docs/tutorial/connect/update-data-connections.md @@ -56,7 +56,7 @@ Doing it is just like updating any other field: # Code above omitted 👆 {!./docs_src/tutorial/connect/update/tutorial001.py[ln:31-32]!} - + # Previous code here omitted 👈 {!./docs_src/tutorial/connect/update/tutorial001.py[ln:62-66]!} @@ -94,8 +94,8 @@ INFO Engine COMMIT // Automatically start a new transaction INFO Engine BEGIN (implicit) // Refresh the hero data -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.team_id +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.08837s ago] (3,) diff --git a/docs/tutorial/create-db-and-table.md b/docs/tutorial/create-db-and-table.md index abd73cb797..52a12fa9c3 100644 --- a/docs/tutorial/create-db-and-table.md +++ b/docs/tutorial/create-db-and-table.md @@ -422,15 +422,15 @@ INFO Engine PRAGMA main.table_info("hero") INFO Engine [raw sql] () INFO Engine PRAGMA temp.table_info("hero") INFO Engine [raw sql] () -INFO Engine +INFO Engine // Finally, the glorious SQL to create the table ✨ CREATE TABLE hero ( - id INTEGER, - name VARCHAR NOT NULL, - secret_name VARCHAR NOT NULL, - age INTEGER, + id INTEGER, + name VARCHAR NOT NULL, + secret_name VARCHAR NOT NULL, + age INTEGER, PRIMARY KEY (id) ) diff --git a/docs/tutorial/delete.md b/docs/tutorial/delete.md index 0c9238d018..590b2ece52 100644 --- a/docs/tutorial/delete.md +++ b/docs/tutorial/delete.md @@ -108,8 +108,8 @@ $ python app.py // The SELECT with WHERE INFO Engine BEGIN (implicit) -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.name = ? INFO Engine [no key 0.00011s] ('Spider-Youngster',) @@ -272,8 +272,8 @@ $ python app.py INFO Engine BEGIN (implicit) // SQL to search for the hero -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.name = ? INFO Engine [no key 0.00013s] ('Spider-Youngster',) ``` diff --git a/docs/tutorial/index.md b/docs/tutorial/index.md index e8594148d6..74107776c2 100644 --- a/docs/tutorial/index.md +++ b/docs/tutorial/index.md @@ -132,7 +132,7 @@ Here are the commands you could use:
```console - // Remember that you might need to use python3.9 or similar 💡 + // Remember that you might need to use python3.9 or similar 💡 // Create the virtual environment using the module "venv" $ python3 -m venv env // ...here it creates the virtual environment in the directory "env" diff --git a/docs/tutorial/indexes.md b/docs/tutorial/indexes.md index 6513d7d462..fef0081dc8 100644 --- a/docs/tutorial/indexes.md +++ b/docs/tutorial/indexes.md @@ -342,10 +342,10 @@ $ python app.py // Create the table CREATE TABLE hero ( - id INTEGER, - name VARCHAR NOT NULL, - secret_name VARCHAR NOT NULL, - age INTEGER, + id INTEGER, + name VARCHAR NOT NULL, + secret_name VARCHAR NOT NULL, + age INTEGER, PRIMARY KEY (id) ) @@ -353,8 +353,8 @@ CREATE TABLE hero ( CREATE INDEX ix_hero_name ON hero (name) // The SELECT with WHERE looks the same -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.name = ? INFO Engine [no key 0.00014s] ('Deadpond',) diff --git a/docs/tutorial/limit-and-offset.md b/docs/tutorial/limit-and-offset.md index 3fb001cf97..dc4c28063c 100644 --- a/docs/tutorial/limit-and-offset.md +++ b/docs/tutorial/limit-and-offset.md @@ -93,7 +93,7 @@ $ python app.py // Previous output omitted 🙈 // Select with LIMIT -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age FROM hero LIMIT ? OFFSET ? INFO Engine [no key 0.00014s] (3, 0) @@ -165,7 +165,7 @@ $python app.py // Previous output omitted 🙈 // Select with LIMIT and OFFSET -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age FROM hero LIMIT ? OFFSET ? INFO Engine [no key 0.00020s] (3, 3) @@ -221,7 +221,7 @@ $ python app.py // Previous output omitted 🙈 // Select last batch with LIMIT and OFFSET -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age FROM hero LIMIT ? OFFSET ? INFO Engine [no key 0.00038s] (3, 6) @@ -241,7 +241,7 @@ You probably noticed the new SQL keywords `LIMIT` and `OFFSET`. You can use them in SQL, at the end of the other parts: ```SQL -SELECT id, name, secret_name, age +SELECT id, name, secret_name, age FROM hero LIMIT 3 OFFSET 6 ``` @@ -285,8 +285,8 @@ $ python app.py // Previous output omitted 🙈 // Select with WHERE and LIMIT -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.age > ? LIMIT ? OFFSET ? INFO Engine [no key 0.00022s] (32, 3, 0) diff --git a/docs/tutorial/many-to-many/create-data.md b/docs/tutorial/many-to-many/create-data.md index 2a51f5acae..971659b9a6 100644 --- a/docs/tutorial/many-to-many/create-data.md +++ b/docs/tutorial/many-to-many/create-data.md @@ -122,16 +122,16 @@ INFO Engine COMMIT // Automatically start a new transaction INFO Engine BEGIN (implicit) // Refresh the data -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.id = ? INFO Engine [generated in 0.00019s] (1,) -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.001959s ago] (2,) -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.003215s ago] (3,) @@ -139,8 +139,8 @@ INFO Engine [cached since 0.003215s ago] (3,) Deadpond: name='Deadpond' age=None id=1 secret_name='Dive Wilson' // Accessing the .team attribute triggers a refresh -INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters -FROM team, heroteamlink +INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters +FROM team, heroteamlink WHERE ? = heroteamlink.hero_id AND team.id = heroteamlink.team_id INFO Engine [generated in 0.00025s] (1,) @@ -151,8 +151,8 @@ Deadpond teams: [Team(id=1, name='Z-Force', headquarters='Sister Margaret’s Ba Rusty-Man: name='Rusty-Man' age=48 id=2 secret_name='Tommy Sharp' // Accessing the .team attribute triggers a refresh -INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters -FROM team, heroteamlink +INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters +FROM team, heroteamlink WHERE ? = heroteamlink.hero_id AND team.id = heroteamlink.team_id INFO Engine [cached since 0.001716s ago] (2,) @@ -163,8 +163,8 @@ Rusty-Man Teams: [Team(id=2, name='Preventers', headquarters='Sharp Tower')] Spider-Boy: name='Spider-Boy' age=None id=3 secret_name='Pedro Parqueador' // Accessing the .team attribute triggers a refresh -INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters -FROM team, heroteamlink +INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters +FROM team, heroteamlink WHERE ? = heroteamlink.hero_id AND team.id = heroteamlink.team_id INFO Engine [cached since 0.002739s ago] (3,) diff --git a/docs/tutorial/many-to-many/create-models-with-link.md b/docs/tutorial/many-to-many/create-models-with-link.md index bc4481f73d..63cbf3eb82 100644 --- a/docs/tutorial/many-to-many/create-models-with-link.md +++ b/docs/tutorial/many-to-many/create-models-with-link.md @@ -151,35 +151,35 @@ $ python app.py // Boilerplate omitted 😉 -INFO Engine +INFO Engine CREATE TABLE team ( - id INTEGER, - name VARCHAR NOT NULL, - headquarters VARCHAR NOT NULL, + id INTEGER, + name VARCHAR NOT NULL, + headquarters VARCHAR NOT NULL, PRIMARY KEY (id) ) INFO Engine [no key 0.00033s] () -INFO Engine +INFO Engine CREATE TABLE hero ( - id INTEGER, - name VARCHAR NOT NULL, - secret_name VARCHAR NOT NULL, - age INTEGER, + id INTEGER, + name VARCHAR NOT NULL, + secret_name VARCHAR NOT NULL, + age INTEGER, PRIMARY KEY (id) ) INFO Engine [no key 0.00016s] () -INFO Engine +INFO Engine // Our shinny new link table ✨ CREATE TABLE heroteamlink ( - team_id INTEGER, - hero_id INTEGER, - PRIMARY KEY (team_id, hero_id), - FOREIGN KEY(team_id) REFERENCES team (id), + team_id INTEGER, + hero_id INTEGER, + PRIMARY KEY (team_id, hero_id), + FOREIGN KEY(team_id) REFERENCES team (id), FOREIGN KEY(hero_id) REFERENCES hero (id) ) diff --git a/docs/tutorial/many-to-many/link-with-extra-fields.md b/docs/tutorial/many-to-many/link-with-extra-fields.md index 9c3309da91..c998175a72 100644 --- a/docs/tutorial/many-to-many/link-with-extra-fields.md +++ b/docs/tutorial/many-to-many/link-with-extra-fields.md @@ -165,16 +165,16 @@ INFO Engine COMMIT INFO Engine BEGIN (implicit) // Automatically fetch the data on attribute access -INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters -FROM team +INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters +FROM team WHERE team.id = ? INFO Engine [generated in 0.00028s] (1,) -INFO Engine SELECT heroteamlink.team_id AS heroteamlink_team_id, heroteamlink.hero_id AS heroteamlink_hero_id, heroteamlink.is_training AS heroteamlink_is_training -FROM heroteamlink +INFO Engine SELECT heroteamlink.team_id AS heroteamlink_team_id, heroteamlink.hero_id AS heroteamlink_hero_id, heroteamlink.is_training AS heroteamlink_is_training +FROM heroteamlink WHERE ? = heroteamlink.team_id INFO Engine [generated in 0.00026s] (1,) -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero WHERE hero.id = ? INFO Engine [generated in 0.00024s] (1,) @@ -182,12 +182,12 @@ INFO Engine [generated in 0.00024s] (1,) Z-Force hero: name='Deadpond' age=None id=1 secret_name='Dive Wilson' is training: False // Automatically fetch the data on attribute access -INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters -FROM team +INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters +FROM team WHERE team.id = ? INFO Engine [cached since 0.008822s ago] (2,) -INFO Engine SELECT heroteamlink.team_id AS heroteamlink_team_id, heroteamlink.hero_id AS heroteamlink_hero_id, heroteamlink.is_training AS heroteamlink_is_training -FROM heroteamlink +INFO Engine SELECT heroteamlink.team_id AS heroteamlink_team_id, heroteamlink.hero_id AS heroteamlink_hero_id, heroteamlink.is_training AS heroteamlink_is_training +FROM heroteamlink WHERE ? = heroteamlink.team_id INFO Engine [cached since 0.005778s ago] (2,) @@ -195,8 +195,8 @@ INFO Engine [cached since 0.005778s ago] (2,) Preventers hero: name='Deadpond' age=None id=1 secret_name='Dive Wilson' is training: True // Automatically fetch the data on attribute access -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.004196s ago] (2,) @@ -204,8 +204,8 @@ INFO Engine [cached since 0.004196s ago] (2,) Preventers hero: name='Spider-Boy' age=None id=2 secret_name='Pedro Parqueador' is training: True // Automatically fetch the data on attribute access -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.006005s ago] (3,) @@ -253,14 +253,14 @@ $ python app.py INFO Engine BEGIN (implicit) // Select the hero -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.name = ? INFO Engine [no key 0.00014s] ('Spider-Boy',) // Select the team -INFO Engine SELECT team.id, team.name, team.headquarters -FROM team +INFO Engine SELECT team.id, team.name, team.headquarters +FROM team WHERE team.name = ? INFO Engine [no key 0.00012s] ('Z-Force',) @@ -269,18 +269,18 @@ INFO Engine INSERT INTO heroteamlink (team_id, hero_id, is_training) VALUES (?, INFO Engine [generated in 0.00023s] (1, 2, 1) // Automatically refresh the data on attribute access -INFO Engine SELECT heroteamlink.team_id AS heroteamlink_team_id, heroteamlink.hero_id AS heroteamlink_hero_id, heroteamlink.is_training AS heroteamlink_is_training -FROM heroteamlink +INFO Engine SELECT heroteamlink.team_id AS heroteamlink_team_id, heroteamlink.hero_id AS heroteamlink_hero_id, heroteamlink.is_training AS heroteamlink_is_training +FROM heroteamlink WHERE ? = heroteamlink.team_id INFO Engine [cached since 0.01514s ago] (1,) INFO Engine COMMIT INFO Engine BEGIN (implicit) -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.08953s ago] (2,) -INFO Engine SELECT heroteamlink.team_id AS heroteamlink_team_id, heroteamlink.hero_id AS heroteamlink_hero_id, heroteamlink.is_training AS heroteamlink_is_training -FROM heroteamlink +INFO Engine SELECT heroteamlink.team_id AS heroteamlink_team_id, heroteamlink.hero_id AS heroteamlink_hero_id, heroteamlink.is_training AS heroteamlink_is_training +FROM heroteamlink WHERE ? = heroteamlink.hero_id INFO Engine [generated in 0.00018s] (2,) @@ -291,18 +291,18 @@ Updated Spider-Boy's Teams: [ ] // Automatically refresh team data on attribute access -INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters -FROM team +INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters +FROM team WHERE team.id = ? INFO Engine [cached since 0.1084s ago] (1,) -INFO Engine SELECT heroteamlink.team_id AS heroteamlink_team_id, heroteamlink.hero_id AS heroteamlink_hero_id, heroteamlink.is_training AS heroteamlink_is_training -FROM heroteamlink +INFO Engine SELECT heroteamlink.team_id AS heroteamlink_team_id, heroteamlink.hero_id AS heroteamlink_hero_id, heroteamlink.is_training AS heroteamlink_is_training +FROM heroteamlink WHERE ? = heroteamlink.team_id INFO Engine [cached since 0.1054s ago] (1,) // Print team hero links Z-Force heroes: [ - HeroTeamLink(team_id=1, is_training=False, hero_id=1), + HeroTeamLink(team_id=1, is_training=False, hero_id=1), HeroTeamLink(team_id=1, is_training=True, hero_id=2) ] ``` @@ -350,8 +350,8 @@ $ python app.py // Previous output omitted 🙈 // Automatically fetch team data on attribute access -INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters -FROM team +INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters +FROM team WHERE team.id = ? INFO Engine [generated in 0.00015s] (2,) @@ -366,16 +366,16 @@ INFO Engine COMMIT INFO Engine BEGIN (implicit) // Automatically fetch data on attribute access -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.2004s ago] (2,) -INFO Engine SELECT heroteamlink.team_id AS heroteamlink_team_id, heroteamlink.hero_id AS heroteamlink_hero_id, heroteamlink.is_training AS heroteamlink_is_training -FROM heroteamlink +INFO Engine SELECT heroteamlink.team_id AS heroteamlink_team_id, heroteamlink.hero_id AS heroteamlink_hero_id, heroteamlink.is_training AS heroteamlink_is_training +FROM heroteamlink WHERE ? = heroteamlink.hero_id INFO Engine [cached since 0.1005s ago] (2,) -INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters -FROM team +INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters +FROM team WHERE team.id = ? INFO Engine [cached since 0.09707s ago] (2,) @@ -383,8 +383,8 @@ INFO Engine [cached since 0.09707s ago] (2,) Spider-Boy team: headquarters='Sharp Tower' id=2 name='Preventers' is training: False // Automatically fetch data on attribute access -INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters -FROM team +INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters +FROM team WHERE team.id = ? INFO Engine [cached since 0.2097s ago] (1,) diff --git a/docs/tutorial/many-to-many/update-remove-relationships.md b/docs/tutorial/many-to-many/update-remove-relationships.md index 5cc55e1f41..91f2f2c0e2 100644 --- a/docs/tutorial/many-to-many/update-remove-relationships.md +++ b/docs/tutorial/many-to-many/update-remove-relationships.md @@ -115,12 +115,12 @@ INFO Engine COMMIT INFO Engine BEGIN (implicit) // Automatically refresh the data while accessing the attribute .teams -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero WHERE hero.id = ? INFO Engine [generated in 0.00044s] (3,) -INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters -FROM team, heroteamlink +INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters +FROM team, heroteamlink WHERE ? = heroteamlink.hero_id AND team.id = heroteamlink.team_id INFO Engine [cached since 0.1648s ago] (3,) @@ -131,8 +131,8 @@ Updated Spider-Boy's Teams: [ ] // Automatically refresh the data while accessing the attribute .heores -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero, heroteamlink +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero, heroteamlink WHERE ? = heroteamlink.team_id AND hero.id = heroteamlink.hero_id INFO Engine [cached since 0.1499s ago] (1,) @@ -208,12 +208,12 @@ INFO Engine COMMIT INFO Engine BEGIN (implicit) // Automatically refresh the data while accessing the attribute .heroes -INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters -FROM team +INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters +FROM team WHERE team.id = ? INFO Engine [generated in 0.00029s] (1,) -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero, heroteamlink +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero, heroteamlink WHERE ? = heroteamlink.team_id AND hero.id = heroteamlink.hero_id INFO Engine [cached since 0.5625s ago] (1,) @@ -223,12 +223,12 @@ Reverted Z-Force's heroes: [ ] // Automatically refresh the data while accessing the attribute .teams -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero WHERE hero.id = ? INFO Engine [cached since 0.4209s ago] (3,) -INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters -FROM team, heroteamlink +INFO Engine SELECT team.id AS team_id, team.name AS team_name, team.headquarters AS team_headquarters +FROM team, heroteamlink WHERE ? = heroteamlink.hero_id AND team.id = heroteamlink.team_id INFO Engine [cached since 0.5842s ago] (3,) diff --git a/docs/tutorial/one.md b/docs/tutorial/one.md index f06343f67f..eadfc62a37 100644 --- a/docs/tutorial/one.md +++ b/docs/tutorial/one.md @@ -86,8 +86,8 @@ $ python app.py // Some boilerplate output omitted 😉 // The SELECT with WHERE -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.age <= ? INFO Engine [no key 0.00021s] (35,) @@ -132,8 +132,8 @@ $ python app.py // Some boilerplate output omitted 😉 // The SELECT with WHERE -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.age <= ? INFO Engine [no key 0.00021s] (35,) @@ -180,8 +180,8 @@ $ python app.py // Some boilerplate output omitted 😉 // The SELECT with WHERE -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.name = ? INFO Engine [no key 0.00015s] ('Deadpond',) @@ -203,8 +203,8 @@ $ python app.py // Some boilerplate output omitted 😉 // The SELECT with WHERE -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.name = ? INFO Engine [no key 0.00015s] ('Deadpond',) @@ -274,8 +274,8 @@ $ python app.py // Some boilerplate output omitted 😉 // SELECT with WHERE -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.age < ? INFO Engine [no key 0.00014s] (25,) @@ -370,8 +370,8 @@ $ python app.py // Some boilerplate output omitted 😉 // SELECT with WHERE -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero WHERE hero.id = ? INFO Engine [generated in 0.00021s] (1,) @@ -413,8 +413,8 @@ $ python app.py // SELECT with WHERE INFO Engine BEGIN (implicit) -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age +FROM hero WHERE hero.id = ? INFO Engine [generated in 0.00024s] (9001,) diff --git a/docs/tutorial/relationship-attributes/back-populates.md b/docs/tutorial/relationship-attributes/back-populates.md index 27129a066c..dbd3e8c1a3 100644 --- a/docs/tutorial/relationship-attributes/back-populates.md +++ b/docs/tutorial/relationship-attributes/back-populates.md @@ -144,10 +144,10 @@ But now, what happens when we print the `preventers_team.heroes`? ``` hl_lines="3" Preventers Team Heroes again: [ - Hero(name='Rusty-Man', age=48, id=2, secret_name='Tommy Sharp', team_id=2), - Hero(name='Spider-Boy', age=None, id=3, secret_name='Pedro Parqueador', team_id=2, team=None), - Hero(name='Tarantula', age=32, id=6, secret_name='Natalia Roman-on', team_id=2), - Hero(name='Dr. Weird', age=36, id=7, secret_name='Steve Weird', team_id=2), + Hero(name='Rusty-Man', age=48, id=2, secret_name='Tommy Sharp', team_id=2), + Hero(name='Spider-Boy', age=None, id=3, secret_name='Pedro Parqueador', team_id=2, team=None), + Hero(name='Tarantula', age=32, id=6, secret_name='Natalia Roman-on', team_id=2), + Hero(name='Dr. Weird', age=36, id=7, secret_name='Steve Weird', team_id=2), Hero(name='Captain North America', age=93, id=8, secret_name='Esteban Rogelios', team_id=2) ] ``` @@ -182,15 +182,15 @@ Now, if we commit it and print again: When we access `preventers_team.heroes` after the `commit`, that triggers a refresh, so we get the latest list, without **Spider-Boy**, so that's fine again: ``` -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age, hero.team_id AS hero_team_id -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age, hero.team_id AS hero_team_id +FROM hero WHERE ? = hero.team_id 2021-08-13 11:15:24,658 INFO sqlalchemy.engine.Engine [cached since 0.1924s ago] (2,) Preventers Team Heroes after commit: [ - Hero(name='Rusty-Man', age=48, id=2, secret_name='Tommy Sharp', team_id=2), - Hero(name='Tarantula', age=32, id=6, secret_name='Natalia Roman-on', team_id=2), - Hero(name='Dr. Weird', age=36, id=7, secret_name='Steve Weird', team_id=2), + Hero(name='Rusty-Man', age=48, id=2, secret_name='Tommy Sharp', team_id=2), + Hero(name='Tarantula', age=32, id=6, secret_name='Natalia Roman-on', team_id=2), + Hero(name='Dr. Weird', age=36, id=7, secret_name='Steve Weird', team_id=2), Hero(name='Captain North America', age=93, id=8, secret_name='Esteban Rogelios', team_id=2) ] ``` @@ -260,9 +260,9 @@ That second print would output: ``` Preventers Team Heroes again: [ - Hero(name='Rusty-Man', age=48, id=2, secret_name='Tommy Sharp', team_id=2), - Hero(name='Tarantula', age=32, id=6, secret_name='Natalia Roman-on', team_id=2), - Hero(name='Dr. Weird', age=36, id=7, secret_name='Steve Weird', team_id=2), + Hero(name='Rusty-Man', age=48, id=2, secret_name='Tommy Sharp', team_id=2), + Hero(name='Tarantula', age=32, id=6, secret_name='Natalia Roman-on', team_id=2), + Hero(name='Dr. Weird', age=36, id=7, secret_name='Steve Weird', team_id=2), Hero(name='Captain North America', age=93, id=8, secret_name='Esteban Rogelios', team_id=2) ] ``` diff --git a/docs/tutorial/relationship-attributes/read-relationships.md b/docs/tutorial/relationship-attributes/read-relationships.md index c970b5eb06..78e4207ae5 100644 --- a/docs/tutorial/relationship-attributes/read-relationships.md +++ b/docs/tutorial/relationship-attributes/read-relationships.md @@ -111,8 +111,8 @@ That would print a list with all the heroes in the Preventers team: $ python app.py // Automatically fetch the heroes -INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age, hero.team_id AS hero_team_id -FROM hero +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age, hero.team_id AS hero_team_id +FROM hero WHERE ? = hero.team_id INFO Engine [cached since 0.8774s ago] (2,) diff --git a/docs/tutorial/select.md b/docs/tutorial/select.md index 5de32db5a1..e2f9af447c 100644 --- a/docs/tutorial/select.md +++ b/docs/tutorial/select.md @@ -274,7 +274,7 @@ This `session.exec(statement)` will generate this output: ``` INFO Engine BEGIN (implicit) -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age FROM hero INFO Engine [no key 0.00032s] () ``` @@ -455,7 +455,7 @@ In this chapter we are touching some of them. ### SQLModel's `select` -When importing from `sqlmodel` the `select()` function, you are using **SQLModel**'s version of `select`. +When importing from `sqlmodel` the `select()` function, you are using **SQLModel**'s version of `select`. SQLAchemy also has it's own `select`, and SQLModel's `select` uses SQLAlchemy's `select` internally. diff --git a/docs/tutorial/update.md b/docs/tutorial/update.md index b3099f5a16..56cc7ac1a5 100644 --- a/docs/tutorial/update.md +++ b/docs/tutorial/update.md @@ -132,8 +132,8 @@ $ python app.py // Some boilerplate and previous output omitted 😉 // The SELECT with WHERE -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.name = ? INFO Engine [no key 0.00017s] ('Spider-Boy',) @@ -275,8 +275,8 @@ $ python app.py // Previous output omitted 🙈 // The SQL to SELECT the fresh hero data -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.id = ? INFO Engine [generated in 0.00018s] (2,) ``` diff --git a/docs/tutorial/where.md b/docs/tutorial/where.md index 47f1b9b113..a3bf6b0529 100644 --- a/docs/tutorial/where.md +++ b/docs/tutorial/where.md @@ -490,8 +490,8 @@ $ python app.py // Now the important part, the SELECT with WHERE 💡 -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.name = ? INFO Engine [no key 0.00014s] ('Deadpond',) @@ -726,8 +726,8 @@ This will select the rows `WHERE` the `age` is **greater than or equal** to `35` The equivalent SQL would be: ```SQL hl_lines="3" -SELECT id, name, secret_name, age -FROM hero +SELECT id, name, secret_name, age +FROM hero WHERE age >= 35 AND age < 40 ``` @@ -743,8 +743,8 @@ $ python app.py // Some boilerplate output omitted 😉 // The SELECT statement with WHERE, also using AND -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.age >= ? AND hero.age < ? INFO Engine [no key 0.00014s] (35, 40) @@ -838,8 +838,8 @@ $ python app.py // Some boilerplate output omitted 😉 // The SELECT statement with WHERE, also using OR 🔍 -INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age -FROM hero +INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age +FROM hero WHERE hero.age <= ? OR hero.age > ? INFO Engine [no key 0.00021s] (35, 90) diff --git a/docs_src/tutorial/automatic_id_none_refresh/annotations/en/tutorial002.md b/docs_src/tutorial/automatic_id_none_refresh/annotations/en/tutorial002.md index 8306d9b898..725fcb6601 100644 --- a/docs_src/tutorial/automatic_id_none_refresh/annotations/en/tutorial002.md +++ b/docs_src/tutorial/automatic_id_none_refresh/annotations/en/tutorial002.md @@ -181,8 +181,8 @@ ``` INFO Engine BEGIN (implicit) - INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age - FROM hero + INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age + FROM hero WHERE hero.id = ? INFO Engine [generated in 0.00017s] (1,) @@ -196,8 +196,8 @@ Generates the output: ``` - INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age - FROM hero + INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age + FROM hero WHERE hero.id = ? INFO Engine [cached since 0.001245s ago] (2,) @@ -211,8 +211,8 @@ Generates the output: ``` - INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age - FROM hero + INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age + FROM hero WHERE hero.id = ? INFO Engine [cached since 0.002215s ago] (3,) @@ -265,8 +265,8 @@ Generates the output: ``` - INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age - FROM hero + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero WHERE hero.id = ? INFO Engine [generated in 0.00024s] (1,) ``` @@ -278,8 +278,8 @@ Generates the output: ``` - INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age - FROM hero + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero WHERE hero.id = ? INFO Engine [cached since 0.001487s ago] (2,) ``` @@ -291,8 +291,8 @@ Generates the output: ``` - INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age - FROM hero + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero WHERE hero.id = ? INFO Engine [cached since 0.002377s ago] (3,) ``` diff --git a/docs_src/tutorial/delete/annotations/en/tutorial002.md b/docs_src/tutorial/delete/annotations/en/tutorial002.md index 130016daec..28dcc50fb3 100644 --- a/docs_src/tutorial/delete/annotations/en/tutorial002.md +++ b/docs_src/tutorial/delete/annotations/en/tutorial002.md @@ -6,8 +6,8 @@ ``` INFO Engine BEGIN (implicit) - INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age - FROM hero + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero WHERE hero.name = ? INFO Engine [no key 0.00011s] ('Spider-Youngster',) ``` @@ -65,8 +65,8 @@ ``` INFO Engine BEGIN (implicit) - INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age - FROM hero + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero WHERE hero.name = ? INFO Engine [no key 0.00013s] ('Spider-Youngster',) ``` diff --git a/docs_src/tutorial/select/annotations/en/tutorial002.md b/docs_src/tutorial/select/annotations/en/tutorial002.md index 2570b542c4..312bd81a94 100644 --- a/docs_src/tutorial/select/annotations/en/tutorial002.md +++ b/docs_src/tutorial/select/annotations/en/tutorial002.md @@ -35,7 +35,7 @@ ``` INFO Engine BEGIN (implicit) - INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age FROM hero INFO Engine [no key 0.00032s] () ``` diff --git a/docs_src/tutorial/update/annotations/en/tutorial002.md b/docs_src/tutorial/update/annotations/en/tutorial002.md index 3a52bd9bdf..eb1a820c0b 100644 --- a/docs_src/tutorial/update/annotations/en/tutorial002.md +++ b/docs_src/tutorial/update/annotations/en/tutorial002.md @@ -5,8 +5,8 @@ This generates the output: ``` - INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age - FROM hero + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero WHERE hero.name = ? INFO Engine [no key 0.00017s] ('Spider-Boy',) ``` @@ -53,8 +53,8 @@ This generates the output: ``` - INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age - FROM hero + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero WHERE hero.id = ? INFO Engine [generated in 0.00018s] (2,) ``` diff --git a/docs_src/tutorial/update/annotations/en/tutorial004.md b/docs_src/tutorial/update/annotations/en/tutorial004.md index 3fcf1040ec..8378d1b3a7 100644 --- a/docs_src/tutorial/update/annotations/en/tutorial004.md +++ b/docs_src/tutorial/update/annotations/en/tutorial004.md @@ -5,8 +5,8 @@ This generates the output: ``` - INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age - FROM hero + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero WHERE hero.name = ? INFO Engine [no key 0.00018s] ('Spider-Boy',) ``` @@ -29,8 +29,8 @@ ``` INFO Engine BEGIN (implicit) - INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age - FROM hero + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero WHERE hero.name = ? INFO Engine [no key 0.00020s] ('Captain North America',) ``` @@ -109,8 +109,8 @@ ``` INFO Engine BEGIN (implicit) - INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age - FROM hero + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero WHERE hero.id = ? INFO Engine [generated in 0.00023s] (2,) ``` @@ -123,8 +123,8 @@ This generates the output: ``` - INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age - FROM hero + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero WHERE hero.id = ? INFO Engine [cached since 0.001709s ago] (7,) ``` diff --git a/sqlmodel/sql/sqltypes.py b/sqlmodel/sql/sqltypes.py index 09b8239476..17d9b06126 100644 --- a/sqlmodel/sql/sqltypes.py +++ b/sqlmodel/sql/sqltypes.py @@ -8,7 +8,6 @@ class AutoString(types.TypeDecorator): # type: ignore - impl = types.String cache_ok = True mysql_default_length = 255 diff --git a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py index e560d04c0e..3a445127a3 100644 --- a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py @@ -261,7 +261,6 @@ def test_tutorial(clear_sqlmodel): ) with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} hero2_data = { "name": "Spider-Boy", diff --git a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py index b58afdf683..1a38f7aab5 100644 --- a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py @@ -184,7 +184,6 @@ def test_tutorial(clear_sqlmodel): ) with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} hero2_data = { "name": "Spider-Boy", diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py index cf008563f4..8ad038f98a 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py @@ -123,7 +123,6 @@ def test_tutorial(clear_sqlmodel): ) with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} hero2_data = { "name": "Spider-Boy", diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py index 57393a7ddc..9fd328238a 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py @@ -123,7 +123,6 @@ def test_tutorial(clear_sqlmodel): ) with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} hero2_data = { "name": "Spider-Boy", diff --git a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py index 5b3c771bb9..0609ae41ff 100644 --- a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py @@ -155,7 +155,6 @@ def test_tutorial(clear_sqlmodel): ) with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} hero2_data = { "name": "Spider-Boy", diff --git a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py index 54fbbdccad..ebb3046ef3 100644 --- a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py @@ -111,7 +111,6 @@ def test_tutorial(clear_sqlmodel): ) with TestClient(mod.app) as client: - hero_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} response = client.post("/heroes/", json=hero_data) data = response.json() diff --git a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py index d8dbe3f7fb..705146cc5b 100644 --- a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py @@ -261,7 +261,6 @@ def test_tutorial(clear_sqlmodel): ) with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} hero2_data = { "name": "Spider-Boy", diff --git a/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py index 2f87fafeff..eb834ec2a4 100644 --- a/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py @@ -99,7 +99,6 @@ def test_tutorial(clear_sqlmodel): ) with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} hero2_data = { "name": "Spider-Boy", diff --git a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py index 6ac1cffc5e..04a4b0c14d 100644 --- a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py @@ -474,7 +474,6 @@ def test_tutorial(clear_sqlmodel): ) with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} hero2_data = { "name": "Spider-Boy", diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py index e622fd37fb..ec75a2f25d 100644 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py @@ -233,7 +233,6 @@ def test_tutorial(clear_sqlmodel): ) with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} hero2_data = { "name": "Spider-Boy", From 7c5894ee75b4d7d69dea9496a6a98f74a5d520e1 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 23 Oct 2023 07:47:04 +0000 Subject: [PATCH 227/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 5e79cf064d..969db489aa 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🎨 Run pre-commit on all files and autoformat. PR [#666](https://github.com/tiangolo/sqlmodel/pull/666) by [@tiangolo](https://github.com/tiangolo). * 👷 Move to Ruff and add pre-commit. PR [#661](https://github.com/tiangolo/sqlmodel/pull/661) by [@tiangolo](https://github.com/tiangolo). * ✨ Raise a more clear error when a type is not valid. PR [#425](https://github.com/tiangolo/sqlmodel/pull/425) by [@ddanier](https://github.com/ddanier). * 📝 Update outdated link in `docs/db-to-code.md`. PR [#649](https://github.com/tiangolo/sqlmodel/pull/649) by [@MatveyF](https://github.com/MatveyF). From 56f43904c1ad8228523d96053541bb535ae6123c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 23 Oct 2023 12:11:36 +0400 Subject: [PATCH 228/906] =?UTF-8?q?=F0=9F=8E=A8=20Update=20docs=20format?= =?UTF-8?q?=20and=20references=20with=20pre-commit=20and=20Ruff=20(#667)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tutorial/fastapi/relationships.md | 4 ++-- docs/tutorial/fastapi/session-with-dependency.md | 2 +- docs/tutorial/fastapi/teams.md | 2 +- docs_src/tutorial/fastapi/app_testing/tutorial001/main.py | 1 - docs_src/tutorial/fastapi/relationships/tutorial001.py | 1 - .../tutorial/fastapi/session_with_dependency/tutorial001.py | 1 - docs_src/tutorial/fastapi/teams/tutorial001.py | 1 - 7 files changed, 4 insertions(+), 8 deletions(-) diff --git a/docs/tutorial/fastapi/relationships.md b/docs/tutorial/fastapi/relationships.md index 6921b5ac85..f152b231c7 100644 --- a/docs/tutorial/fastapi/relationships.md +++ b/docs/tutorial/fastapi/relationships.md @@ -84,7 +84,7 @@ In this case, we used `response_model=TeamRead` and `response_model=HeroRead`, s # Code here omitted 👈 -{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:159-164]!} +{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:158-163]!} # Code below omitted 👇 ``` @@ -234,7 +234,7 @@ In the case of the hero, this tells FastAPI to extract the `team` too. And in th # Code here omitted 👈 -{!./docs_src/tutorial/fastapi/relationships/tutorial001.py[ln:168-173]!} +{!./docs_src/tutorial/fastapi/relationships/tutorial001.py[ln:167-172]!} # Code below omitted 👇 ``` diff --git a/docs/tutorial/fastapi/session-with-dependency.md b/docs/tutorial/fastapi/session-with-dependency.md index 52a800b9ea..195c2e1729 100644 --- a/docs/tutorial/fastapi/session-with-dependency.md +++ b/docs/tutorial/fastapi/session-with-dependency.md @@ -177,7 +177,7 @@ And then we remove the previous `with` block with the old **session**. # Code here omitted 👈 -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:55-107]!} +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:55-106]!} ```
diff --git a/docs/tutorial/fastapi/teams.md b/docs/tutorial/fastapi/teams.md index 0b19a95cb3..52554cf4b6 100644 --- a/docs/tutorial/fastapi/teams.md +++ b/docs/tutorial/fastapi/teams.md @@ -92,7 +92,7 @@ These are equivalent and very similar to the **path operations** for the **heroe ```Python hl_lines="3-9 12-20 23-28 31-47 50-57" # Code above omitted 👆 -{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:139-193]!} +{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:138-192]!} # Code below omitted 👇 ``` diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py b/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py index 88b8fbbcea..d106c4ebab 100644 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py @@ -98,7 +98,6 @@ def update_hero( @app.delete("/heroes/{hero_id}") def delete_hero(*, session: Session = Depends(get_session), hero_id: int): - hero = session.get(Hero, hero_id) if not hero: raise HTTPException(status_code=404, detail="Hero not found") diff --git a/docs_src/tutorial/fastapi/relationships/tutorial001.py b/docs_src/tutorial/fastapi/relationships/tutorial001.py index 97220b95e5..6a03846686 100644 --- a/docs_src/tutorial/fastapi/relationships/tutorial001.py +++ b/docs_src/tutorial/fastapi/relationships/tutorial001.py @@ -136,7 +136,6 @@ def update_hero( @app.delete("/heroes/{hero_id}") def delete_hero(*, session: Session = Depends(get_session), hero_id: int): - hero = session.get(Hero, hero_id) if not hero: raise HTTPException(status_code=404, detail="Hero not found") diff --git a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py index 88b8fbbcea..d106c4ebab 100644 --- a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py +++ b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py @@ -98,7 +98,6 @@ def update_hero( @app.delete("/heroes/{hero_id}") def delete_hero(*, session: Session = Depends(get_session), hero_id: int): - hero = session.get(Hero, hero_id) if not hero: raise HTTPException(status_code=404, detail="Hero not found") diff --git a/docs_src/tutorial/fastapi/teams/tutorial001.py b/docs_src/tutorial/fastapi/teams/tutorial001.py index 2a0bd600fc..6f84182bc9 100644 --- a/docs_src/tutorial/fastapi/teams/tutorial001.py +++ b/docs_src/tutorial/fastapi/teams/tutorial001.py @@ -127,7 +127,6 @@ def update_hero( @app.delete("/heroes/{hero_id}") def delete_hero(*, session: Session = Depends(get_session), hero_id: int): - hero = session.get(Hero, hero_id) if not hero: raise HTTPException(status_code=404, detail="Hero not found") From 2676cf2b06d4ffaacb084b175b38cd2d609c5aa4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 23 Oct 2023 08:12:08 +0000 Subject: [PATCH 229/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 969db489aa..06c9697399 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🎨 Update docs format and references with pre-commit and Ruff. PR [#667](https://github.com/tiangolo/sqlmodel/pull/667) by [@tiangolo](https://github.com/tiangolo). * 🎨 Run pre-commit on all files and autoformat. PR [#666](https://github.com/tiangolo/sqlmodel/pull/666) by [@tiangolo](https://github.com/tiangolo). * 👷 Move to Ruff and add pre-commit. PR [#661](https://github.com/tiangolo/sqlmodel/pull/661) by [@tiangolo](https://github.com/tiangolo). * ✨ Raise a more clear error when a type is not valid. PR [#425](https://github.com/tiangolo/sqlmodel/pull/425) by [@ddanier](https://github.com/ddanier). From b83e848699fc314d27e01e3db0b9caddd4601358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 23 Oct 2023 12:15:05 +0400 Subject: [PATCH 230/906] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20MkDocs?= =?UTF-8?q?=20Material=20(#668)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 3 +-- scripts/docs-live.sh | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 73d8b3ac92..7aa9f96885 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,8 +39,7 @@ sqlalchemy2-stubs = {version = "*", allow-prereleases = true} pytest = "^7.0.1" mypy = "0.971" black = "^22.10.0" -mkdocs = "^1.2.1" -mkdocs-material = "^8.1.4" +mkdocs-material = "9.1.21" pillow = "^9.3.0" cairosvg = "^2.5.2" mdx-include = "^1.4.1" diff --git a/scripts/docs-live.sh b/scripts/docs-live.sh index 5342a9e59f..f9d31ba361 100755 --- a/scripts/docs-live.sh +++ b/scripts/docs-live.sh @@ -2,4 +2,6 @@ set -e +export DYLD_FALLBACK_LIBRARY_PATH="/opt/homebrew/lib" + mkdocs serve --dev-addr 127.0.0.1:8008 From 40c1af92021ae02e771d43971b0f96a786c78875 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 23 Oct 2023 08:15:40 +0000 Subject: [PATCH 231/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 06c9697399..108942f425 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆️ Upgrade MkDocs Material. PR [#668](https://github.com/tiangolo/sqlmodel/pull/668) by [@tiangolo](https://github.com/tiangolo). * 🎨 Update docs format and references with pre-commit and Ruff. PR [#667](https://github.com/tiangolo/sqlmodel/pull/667) by [@tiangolo](https://github.com/tiangolo). * 🎨 Run pre-commit on all files and autoformat. PR [#666](https://github.com/tiangolo/sqlmodel/pull/666) by [@tiangolo](https://github.com/tiangolo). * 👷 Move to Ruff and add pre-commit. PR [#661](https://github.com/tiangolo/sqlmodel/pull/661) by [@tiangolo](https://github.com/tiangolo). From d3261cab591051759ad966eac8f61e6ee39ebaa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 23 Oct 2023 13:22:44 +0400 Subject: [PATCH 232/906] =?UTF-8?q?=F0=9F=90=9B=20Fix=20enum=20type=20chec?= =?UTF-8?q?ks=20ordering=20in=20`get=5Fsqlalchemy=5Ftype`=20(#669)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Pierre Cheynier --- sqlmodel/main.py | 5 +++-- tests/test_enums.py | 44 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index d5a7302438..a32be42c65 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -384,6 +384,9 @@ def __init__( def get_sqlalchemy_type(field: ModelField) -> Any: if isinstance(field.type_, type) and field.shape == SHAPE_SINGLETON: + # Check enums first as an enum can also be a str, needed by Pydantic/FastAPI + if issubclass(field.type_, Enum): + return sa_Enum(field.type_) if issubclass(field.type_, str): if field.field_info.max_length: return AutoString(length=field.field_info.max_length) @@ -402,8 +405,6 @@ def get_sqlalchemy_type(field: ModelField) -> Any: return Interval if issubclass(field.type_, time): return Time - if issubclass(field.type_, Enum): - return sa_Enum(field.type_) if issubclass(field.type_, bytes): return LargeBinary if issubclass(field.type_, Decimal): diff --git a/tests/test_enums.py b/tests/test_enums.py index aeec6456da..194bdefea1 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -14,12 +14,12 @@ """ -class MyEnum1(enum.Enum): +class MyEnum1(str, enum.Enum): A = "A" B = "B" -class MyEnum2(enum.Enum): +class MyEnum2(str, enum.Enum): C = "C" D = "D" @@ -70,3 +70,43 @@ def test_sqlite_ddl_sql(capsys): captured = capsys.readouterr() assert "enum_field VARCHAR(1) NOT NULL" in captured.out assert "CREATE TYPE" not in captured.out + + +def test_json_schema_flat_model(): + assert FlatModel.schema() == { + "title": "FlatModel", + "type": "object", + "properties": { + "id": {"title": "Id", "type": "string", "format": "uuid"}, + "enum_field": {"$ref": "#/definitions/MyEnum1"}, + }, + "required": ["id", "enum_field"], + "definitions": { + "MyEnum1": { + "title": "MyEnum1", + "description": "An enumeration.", + "enum": ["A", "B"], + "type": "string", + } + }, + } + + +def test_json_schema_inherit_model(): + assert InheritModel.schema() == { + "title": "InheritModel", + "type": "object", + "properties": { + "id": {"title": "Id", "type": "string", "format": "uuid"}, + "enum_field": {"$ref": "#/definitions/MyEnum2"}, + }, + "required": ["id", "enum_field"], + "definitions": { + "MyEnum2": { + "title": "MyEnum2", + "description": "An enumeration.", + "enum": ["C", "D"], + "type": "string", + } + }, + } From c25a8cb89b7b637dd0b74424f45357aae3141d22 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 23 Oct 2023 09:23:24 +0000 Subject: [PATCH 233/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 108942f425..6865188502 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix enum type checks ordering in `get_sqlalchemy_type`. PR [#669](https://github.com/tiangolo/sqlmodel/pull/669) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade MkDocs Material. PR [#668](https://github.com/tiangolo/sqlmodel/pull/668) by [@tiangolo](https://github.com/tiangolo). * 🎨 Update docs format and references with pre-commit and Ruff. PR [#667](https://github.com/tiangolo/sqlmodel/pull/667) by [@tiangolo](https://github.com/tiangolo). * 🎨 Run pre-commit on all files and autoformat. PR [#666](https://github.com/tiangolo/sqlmodel/pull/666) by [@tiangolo](https://github.com/tiangolo). From 9511c4677db5370ccc2fb320e83eb1730f7e1d80 Mon Sep 17 00:00:00 2001 From: Daniil Fajnberg <60156134+daniil-berg@users.noreply.github.com> Date: Mon, 23 Oct 2023 09:39:55 +0000 Subject: [PATCH 234/906] =?UTF-8?q?=E2=AC=86=20Raise=20SQLAlchemy=20versio?= =?UTF-8?q?n=20requirement=20to=20at=20least=20`1.4.29`=20(related=20to=20?= =?UTF-8?q?#434)=20(#439)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7aa9f96885..cd59bd4cf4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ classifiers = [ [tool.poetry.dependencies] python = "^3.7" -SQLAlchemy = ">=1.4.17,<=1.4.41" +SQLAlchemy = ">=1.4.29,<=1.4.41" pydantic = "^1.8.2" sqlalchemy2-stubs = {version = "*", allow-prereleases = true} From d281a0fa9f7a0c6384f6e4c30044e124fa26bb1c Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 23 Oct 2023 09:40:31 +0000 Subject: [PATCH 235/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 6865188502..4728d48f50 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Raise SQLAlchemy version requirement to at least `1.4.29` (related to #434). PR [#439](https://github.com/tiangolo/sqlmodel/pull/439) by [@daniil-berg](https://github.com/daniil-berg). * 🐛 Fix enum type checks ordering in `get_sqlalchemy_type`. PR [#669](https://github.com/tiangolo/sqlmodel/pull/669) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade MkDocs Material. PR [#668](https://github.com/tiangolo/sqlmodel/pull/668) by [@tiangolo](https://github.com/tiangolo). * 🎨 Update docs format and references with pre-commit and Ruff. PR [#667](https://github.com/tiangolo/sqlmodel/pull/667) by [@tiangolo](https://github.com/tiangolo). From d1cf6134617cc374fa3aee2b374dc1387c9bc868 Mon Sep 17 00:00:00 2001 From: Sandro Tosi Date: Mon, 23 Oct 2023 07:22:10 -0400 Subject: [PATCH 236/906] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20support?= =?UTF-8?q?=20for=20SQLAlchemy=201.4.49,=20update=20tests=20(#519)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- .../test_multiple_models/test_tutorial001.py | 14 ++++++++++++-- .../test_multiple_models/test_tutorial002.py | 14 ++++++++++++-- .../test_tutorial/test_indexes/test_tutorial001.py | 14 ++++++++++++-- .../test_tutorial/test_indexes/test_tutorial006.py | 14 ++++++++++++-- 5 files changed, 49 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cd59bd4cf4..94fe55d2e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ classifiers = [ [tool.poetry.dependencies] python = "^3.7" -SQLAlchemy = ">=1.4.29,<=1.4.41" +SQLAlchemy = ">=1.4.29,<2.0.0" pydantic = "^1.8.2" sqlalchemy2-stubs = {version = "*", allow-prereleases = true} diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py index 8ad038f98a..8d99cf9f5b 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py @@ -172,8 +172,18 @@ def test_tutorial(clear_sqlmodel): insp: Inspector = inspect(mod.engine) indexes = insp.get_indexes(str(mod.Hero.__tablename__)) expected_indexes = [ - {"name": "ix_hero_name", "column_names": ["name"], "unique": 0}, - {"name": "ix_hero_age", "column_names": ["age"], "unique": 0}, + { + "name": "ix_hero_name", + "dialect_options": {}, + "column_names": ["name"], + "unique": 0, + }, + { + "name": "ix_hero_age", + "dialect_options": {}, + "column_names": ["age"], + "unique": 0, + }, ] for index in expected_indexes: assert index in indexes, "This expected index should be in the indexes in DB" diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py index 9fd328238a..94a41b3076 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py @@ -172,8 +172,18 @@ def test_tutorial(clear_sqlmodel): insp: Inspector = inspect(mod.engine) indexes = insp.get_indexes(str(mod.Hero.__tablename__)) expected_indexes = [ - {"name": "ix_hero_age", "column_names": ["age"], "unique": 0}, - {"name": "ix_hero_name", "column_names": ["name"], "unique": 0}, + { + "name": "ix_hero_age", + "dialect_options": {}, + "column_names": ["age"], + "unique": 0, + }, + { + "name": "ix_hero_name", + "dialect_options": {}, + "column_names": ["name"], + "unique": 0, + }, ] for index in expected_indexes: assert index in indexes, "This expected index should be in the indexes in DB" diff --git a/tests/test_tutorial/test_indexes/test_tutorial001.py b/tests/test_tutorial/test_indexes/test_tutorial001.py index 596207737d..f33db5bcc7 100644 --- a/tests/test_tutorial/test_indexes/test_tutorial001.py +++ b/tests/test_tutorial/test_indexes/test_tutorial001.py @@ -25,8 +25,18 @@ def test_tutorial(clear_sqlmodel): insp: Inspector = inspect(mod.engine) indexes = insp.get_indexes(str(mod.Hero.__tablename__)) expected_indexes = [ - {"name": "ix_hero_name", "column_names": ["name"], "unique": 0}, - {"name": "ix_hero_age", "column_names": ["age"], "unique": 0}, + { + "name": "ix_hero_name", + "dialect_options": {}, + "column_names": ["name"], + "unique": 0, + }, + { + "name": "ix_hero_age", + "dialect_options": {}, + "column_names": ["age"], + "unique": 0, + }, ] for index in expected_indexes: assert index in indexes, "This expected index should be in the indexes in DB" diff --git a/tests/test_tutorial/test_indexes/test_tutorial006.py b/tests/test_tutorial/test_indexes/test_tutorial006.py index e26f8b2ed8..893043dad1 100644 --- a/tests/test_tutorial/test_indexes/test_tutorial006.py +++ b/tests/test_tutorial/test_indexes/test_tutorial006.py @@ -26,8 +26,18 @@ def test_tutorial(clear_sqlmodel): insp: Inspector = inspect(mod.engine) indexes = insp.get_indexes(str(mod.Hero.__tablename__)) expected_indexes = [ - {"name": "ix_hero_name", "column_names": ["name"], "unique": 0}, - {"name": "ix_hero_age", "column_names": ["age"], "unique": 0}, + { + "name": "ix_hero_name", + "dialect_options": {}, + "column_names": ["name"], + "unique": 0, + }, + { + "name": "ix_hero_age", + "dialect_options": {}, + "column_names": ["age"], + "unique": 0, + }, ] for index in expected_indexes: assert index in indexes, "This expected index should be in the indexes in DB" From c213f5daf45091cb269fb8ee979efd0049b89927 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 23 Oct 2023 11:22:53 +0000 Subject: [PATCH 237/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 4728d48f50..747d1031d8 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆️ Upgrade support for SQLAlchemy 1.4.49, update tests. PR [#519](https://github.com/tiangolo/sqlmodel/pull/519) by [@sandrotosi](https://github.com/sandrotosi). * ⬆ Raise SQLAlchemy version requirement to at least `1.4.29` (related to #434). PR [#439](https://github.com/tiangolo/sqlmodel/pull/439) by [@daniil-berg](https://github.com/daniil-berg). * 🐛 Fix enum type checks ordering in `get_sqlalchemy_type`. PR [#669](https://github.com/tiangolo/sqlmodel/pull/669) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade MkDocs Material. PR [#668](https://github.com/tiangolo/sqlmodel/pull/668) by [@tiangolo](https://github.com/tiangolo). From 9809b5bc8323712fe41d1e7a92352da8a07ed69a Mon Sep 17 00:00:00 2001 From: Daniil Fajnberg <60156134+daniil-berg@users.noreply.github.com> Date: Mon, 23 Oct 2023 13:59:06 +0000 Subject: [PATCH 238/906] =?UTF-8?q?=F0=9F=90=9B=20Fix=20allowing=20using?= =?UTF-8?q?=20a=20`ForeignKey`=20directly,=20remove=20repeated=20column=20?= =?UTF-8?q?construction=20from=20`SQLModelMetaclass.=5F=5Finit=5F=5F`=20an?= =?UTF-8?q?d=20upgrade=20minimum=20SQLAlchemy=20to=20`>=3D1.4.36`=20(#443)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- pyproject.toml | 2 +- sqlmodel/main.py | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 94fe55d2e0..57426b5d2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ classifiers = [ [tool.poetry.dependencies] python = "^3.7" -SQLAlchemy = ">=1.4.29,<2.0.0" +SQLAlchemy = ">=1.4.36,<2.0.0" pydantic = "^1.8.2" sqlalchemy2-stubs = {version = "*", allow-prereleases = true} diff --git a/sqlmodel/main.py b/sqlmodel/main.py index a32be42c65..07e600e4d4 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -334,14 +334,10 @@ def __init__( base_is_table = True break if getattr(cls.__config__, "table", False) and not base_is_table: - dict_used = dict_.copy() - for field_name, field_value in cls.__fields__.items(): - dict_used[field_name] = get_column_from_field(field_value) for rel_name, rel_info in cls.__sqlmodel_relationships__.items(): if rel_info.sa_relationship: # There's a SQLAlchemy relationship declared, that takes precedence # over anything else, use that and continue with the next attribute - dict_used[rel_name] = rel_info.sa_relationship setattr(cls, rel_name, rel_info.sa_relationship) # Fix #315 continue ann = cls.__annotations__[rel_name] @@ -375,9 +371,11 @@ def __init__( rel_value: RelationshipProperty = relationship( # type: ignore relationship_to, *rel_args, **rel_kwargs ) - dict_used[rel_name] = rel_value setattr(cls, rel_name, rel_value) # Fix #315 - DeclarativeMeta.__init__(cls, classname, bases, dict_used, **kw) + # SQLAlchemy no longer uses dict_ + # Ref: https://github.com/sqlalchemy/sqlalchemy/commit/428ea01f00a9cc7f85e435018565eb6da7af1b77 + # Tag: 1.4.36 + DeclarativeMeta.__init__(cls, classname, bases, dict_, **kw) else: ModelMetaclass.__init__(cls, classname, bases, dict_, **kw) From b8996f0e62bca6155e2d27200d15de8b54c40ac3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 23 Oct 2023 13:59:53 +0000 Subject: [PATCH 239/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 747d1031d8..4ab34b552c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix allowing using a `ForeignKey` directly, remove repeated column construction from `SQLModelMetaclass.__init__` and upgrade minimum SQLAlchemy to `>=1.4.36`. PR [#443](https://github.com/tiangolo/sqlmodel/pull/443) by [@daniil-berg](https://github.com/daniil-berg). * ⬆️ Upgrade support for SQLAlchemy 1.4.49, update tests. PR [#519](https://github.com/tiangolo/sqlmodel/pull/519) by [@sandrotosi](https://github.com/sandrotosi). * ⬆ Raise SQLAlchemy version requirement to at least `1.4.29` (related to #434). PR [#439](https://github.com/tiangolo/sqlmodel/pull/439) by [@daniil-berg](https://github.com/daniil-berg). * 🐛 Fix enum type checks ordering in `get_sqlalchemy_type`. PR [#669](https://github.com/tiangolo/sqlmodel/pull/669) by [@tiangolo](https://github.com/tiangolo). From 9732c5ac60354b4a5c51e12ea35ca6fc4fb8d73b Mon Sep 17 00:00:00 2001 From: Arseny Boykov <36469655+Bobronium@users.noreply.github.com> Date: Mon, 23 Oct 2023 17:58:16 +0300 Subject: [PATCH 240/906] =?UTF-8?q?=F0=9F=90=9B=20Fix=20`AsyncSession`=20t?= =?UTF-8?q?ype=20annotations=20for=20`exec()`=20(#58)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- sqlmodel/ext/asyncio/session.py | 46 ++++++++++++++++++++++++++++----- sqlmodel/orm/session.py | 2 +- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/sqlmodel/ext/asyncio/session.py b/sqlmodel/ext/asyncio/session.py index 80267b25e5..f500c44dc2 100644 --- a/sqlmodel/ext/asyncio/session.py +++ b/sqlmodel/ext/asyncio/session.py @@ -1,17 +1,17 @@ -from typing import Any, Mapping, Optional, Sequence, TypeVar, Union +from typing import Any, Mapping, Optional, Sequence, TypeVar, Union, overload from sqlalchemy import util from sqlalchemy.ext.asyncio import AsyncSession as _AsyncSession from sqlalchemy.ext.asyncio import engine from sqlalchemy.ext.asyncio.engine import AsyncConnection, AsyncEngine from sqlalchemy.util.concurrency import greenlet_spawn -from sqlmodel.sql.base import Executable -from ...engine.result import ScalarResult +from ...engine.result import Result, ScalarResult from ...orm.session import Session -from ...sql.expression import Select +from ...sql.base import Executable +from ...sql.expression import Select, SelectOfScalar -_T = TypeVar("_T") +_TSelectParam = TypeVar("_TSelectParam") class AsyncSession(_AsyncSession): @@ -40,14 +40,46 @@ def __init__( Session(bind=bind, binds=binds, **kw) # type: ignore ) + @overload async def exec( self, - statement: Union[Select[_T], Executable[_T]], + statement: Select[_TSelectParam], + *, + params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, + execution_options: Mapping[str, Any] = util.EMPTY_DICT, + bind_arguments: Optional[Mapping[str, Any]] = None, + _parent_execute_state: Optional[Any] = None, + _add_event: Optional[Any] = None, + **kw: Any, + ) -> Result[_TSelectParam]: + ... + + @overload + async def exec( + self, + statement: SelectOfScalar[_TSelectParam], + *, + params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, + execution_options: Mapping[str, Any] = util.EMPTY_DICT, + bind_arguments: Optional[Mapping[str, Any]] = None, + _parent_execute_state: Optional[Any] = None, + _add_event: Optional[Any] = None, + **kw: Any, + ) -> ScalarResult[_TSelectParam]: + ... + + async def exec( + self, + statement: Union[ + Select[_TSelectParam], + SelectOfScalar[_TSelectParam], + Executable[_TSelectParam], + ], params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, execution_options: Mapping[Any, Any] = util.EMPTY_DICT, bind_arguments: Optional[Mapping[str, Any]] = None, **kw: Any, - ) -> ScalarResult[_T]: + ) -> Union[Result[_TSelectParam], ScalarResult[_TSelectParam]]: # TODO: the documentation says execution_options accepts a dict, but only # util.immutabledict has the union() method. Is this a bug in SQLAlchemy? execution_options = execution_options.union({"prebuffer_rows": True}) # type: ignore diff --git a/sqlmodel/orm/session.py b/sqlmodel/orm/session.py index 1692fdcbcb..0c70c290ae 100644 --- a/sqlmodel/orm/session.py +++ b/sqlmodel/orm/session.py @@ -4,11 +4,11 @@ from sqlalchemy.orm import Query as _Query from sqlalchemy.orm import Session as _Session from sqlalchemy.sql.base import Executable as _Executable -from sqlmodel.sql.expression import Select, SelectOfScalar from typing_extensions import Literal from ..engine.result import Result, ScalarResult from ..sql.base import Executable +from ..sql.expression import Select, SelectOfScalar _TSelectParam = TypeVar("_TSelectParam") From 8e55ea51259ad19bf3f4769be137e77f3edb60fd Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 23 Oct 2023 14:58:52 +0000 Subject: [PATCH 241/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 4ab34b552c..c8cea2cb8e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix `AsyncSession` type annotations for `exec()`. PR [#58](https://github.com/tiangolo/sqlmodel/pull/58) by [@Bobronium](https://github.com/Bobronium). * 🐛 Fix allowing using a `ForeignKey` directly, remove repeated column construction from `SQLModelMetaclass.__init__` and upgrade minimum SQLAlchemy to `>=1.4.36`. PR [#443](https://github.com/tiangolo/sqlmodel/pull/443) by [@daniil-berg](https://github.com/daniil-berg). * ⬆️ Upgrade support for SQLAlchemy 1.4.49, update tests. PR [#519](https://github.com/tiangolo/sqlmodel/pull/519) by [@sandrotosi](https://github.com/sandrotosi). * ⬆ Raise SQLAlchemy version requirement to at least `1.4.29` (related to #434). PR [#439](https://github.com/tiangolo/sqlmodel/pull/439) by [@daniil-berg](https://github.com/daniil-berg). From 1062e1b4853484d1653115e3517fb1cfa482416b Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Mon, 23 Oct 2023 16:16:17 +0100 Subject: [PATCH 242/906] =?UTF-8?q?=F0=9F=94=A7=20Update=20mypy=20config,?= =?UTF-8?q?=20use=20`strict=20=3D=20true`=20instead=20of=20manual=20config?= =?UTF-8?q?s=20(#428)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- pyproject.toml | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 57426b5d2a..c7956daaa9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,21 +73,7 @@ exclude_lines = [ ] [tool.mypy] -# --strict -disallow_any_generics = true -disallow_subclassing_any = true -disallow_untyped_calls = true -disallow_untyped_defs = true -disallow_incomplete_defs = true -check_untyped_defs = true -disallow_untyped_decorators = true -no_implicit_optional = true -warn_redundant_casts = true -warn_unused_ignores = true -warn_return_any = true -implicit_reexport = false -strict_equality = true -# --strict end +strict = true [[tool.mypy.overrides]] module = "sqlmodel.sql.expression" From d6e4f9b9e3a6162b9ac7839d5bde19f27e7af3a2 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 23 Oct 2023 15:16:53 +0000 Subject: [PATCH 243/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index c8cea2cb8e..7cc9b97944 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update mypy config, use `strict = true` instead of manual configs. PR [#428](https://github.com/tiangolo/sqlmodel/pull/428) by [@michaeloliverx](https://github.com/michaeloliverx). * 🐛 Fix `AsyncSession` type annotations for `exec()`. PR [#58](https://github.com/tiangolo/sqlmodel/pull/58) by [@Bobronium](https://github.com/Bobronium). * 🐛 Fix allowing using a `ForeignKey` directly, remove repeated column construction from `SQLModelMetaclass.__init__` and upgrade minimum SQLAlchemy to `>=1.4.36`. PR [#443](https://github.com/tiangolo/sqlmodel/pull/443) by [@daniil-berg](https://github.com/daniil-berg). * ⬆️ Upgrade support for SQLAlchemy 1.4.49, update tests. PR [#519](https://github.com/tiangolo/sqlmodel/pull/519) by [@sandrotosi](https://github.com/sandrotosi). From 475b838c8b6ddb8c34aee5fa01c65519d7dbf351 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 19:32:09 +0400 Subject: [PATCH 244/906] =?UTF-8?q?=E2=AC=86=20Bump=20actions/checkout=20f?= =?UTF-8?q?rom=203=20to=204=20(#670)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-docs.yml | 4 ++-- .github/workflows/deploy-docs.yml | 2 +- .github/workflows/latest-changes.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/test.yml | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index cf5295aae0..879cc7c5d8 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -17,7 +17,7 @@ jobs: outputs: docs: ${{ steps.filter.outputs.docs }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # For pull requests it's not necessary to checkout the code but for the main branch it is - uses: dorny/paths-filter@v2 id: filter @@ -41,7 +41,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 25cd1ff369..e7641a3c0f 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -14,7 +14,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Clean site run: | rm -rf ./site diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml index 357767bba9..7e8403972d 100644 --- a/.github/workflows/latest-changes.yml +++ b/.github/workflows/latest-changes.yml @@ -20,7 +20,7 @@ jobs: latest-changes: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: # To allow latest-changes to commit to the main branch token: ${{ secrets.SQLMODEL_LATEST_CHANGES }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1c21cd4a87..42a0eec3ce 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,7 +15,7 @@ jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 871557a6fe..201abc7c22 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,7 +28,7 @@ jobs: fail-fast: false steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: @@ -74,7 +74,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: From 189059e07e194c097d0c4f9375294ae27897b46e Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 23 Oct 2023 15:32:48 +0000 Subject: [PATCH 245/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 7cc9b97944..14d5e9c0f1 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump actions/checkout from 3 to 4. PR [#670](https://github.com/tiangolo/sqlmodel/pull/670) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔧 Update mypy config, use `strict = true` instead of manual configs. PR [#428](https://github.com/tiangolo/sqlmodel/pull/428) by [@michaeloliverx](https://github.com/michaeloliverx). * 🐛 Fix `AsyncSession` type annotations for `exec()`. PR [#58](https://github.com/tiangolo/sqlmodel/pull/58) by [@Bobronium](https://github.com/Bobronium). * 🐛 Fix allowing using a `ForeignKey` directly, remove repeated column construction from `SQLModelMetaclass.__init__` and upgrade minimum SQLAlchemy to `>=1.4.36`. PR [#443](https://github.com/tiangolo/sqlmodel/pull/443) by [@daniil-berg](https://github.com/daniil-berg). From dee70033b81813674d2740e7556c9d4c3995dc62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 23 Oct 2023 19:33:08 +0400 Subject: [PATCH 246/906] =?UTF-8?q?=E2=9C=85=20Refactor=20OpenAPI=20FastAP?= =?UTF-8?q?I=20tests=20to=20simplify=20updating=20them=20later,=20this=20m?= =?UTF-8?q?oves=20things=20around=20without=20changes=20(#671)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_delete/test_tutorial001.py | 523 +++++---- .../test_limit_and_offset/test_tutorial001.py | 365 +++--- .../test_relationships/test_tutorial001.py | 1030 +++++++++-------- .../test_tutorial001.py | 523 +++++---- .../test_teams/test_tutorial001.py | 965 +++++++-------- .../test_update/test_tutorial001.py | 467 ++++---- 6 files changed, 1999 insertions(+), 1874 deletions(-) diff --git a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py index 3a445127a3..8979245dd6 100644 --- a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py @@ -2,255 +2,6 @@ from sqlmodel import create_engine from sqlmodel.pool import StaticPool -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Offset", "type": "integer", "default": 0}, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "type": "integer", - "default": 100, - "lte": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": {"$ref": "#/components/schemas/HeroRead"}, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroCreate"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroRead"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroRead"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "delete": { - "summary": "Delete Hero", - "operationId": "delete_hero_heroes__hero_id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Hero", - "operationId": "update_hero_heroes__hero_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroUpdate"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroRead"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - }, - }, - "HeroRead": { - "title": "HeroRead", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "id": {"title": "Id", "type": "integer"}, - }, - }, - "HeroUpdate": { - "title": "HeroUpdate", - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"type": "string"}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - def test_tutorial(clear_sqlmodel): from docs_src.tutorial.fastapi.delete import tutorial001 as mod @@ -284,10 +35,6 @@ def test_tutorial(clear_sqlmodel): assert response.status_code == 200, response.text response = client.get("/heroes/9000") assert response.status_code == 404, response.text - response = client.get("/openapi.json") - data = response.json() - assert response.status_code == 200, response.text - assert data == openapi_schema response = client.get("/heroes/") assert response.status_code == 200, response.text data = response.json() @@ -308,3 +55,273 @@ def test_tutorial(clear_sqlmodel): response = client.delete("/heroes/9000") assert response.status_code == 404, response.text + + response = client.get("/openapi.json") + data = response.json() + assert response.status_code == 200, response.text + assert data == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Offset", + "type": "integer", + "default": 0, + }, + "name": "offset", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + "lte": 100, + }, + "name": "limit", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Heroes Heroes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/HeroRead" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/heroes/{hero_id}": { + "get": { + "summary": "Read Hero", + "operationId": "read_hero_heroes__hero_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "delete": { + "summary": "Delete Hero", + "operationId": "delete_hero_heroes__hero_id__delete", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "patch": { + "summary": "Update Hero", + "operationId": "update_hero_heroes__hero_id__patch", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroUpdate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "HeroCreate": { + "title": "HeroCreate", + "required": ["name", "secret_name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "HeroRead": { + "title": "HeroRead", + "required": ["name", "secret_name", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "id": {"title": "Id", "type": "integer"}, + }, + }, + "HeroUpdate": { + "title": "HeroUpdate", + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py index 1a38f7aab5..d7f642a788 100644 --- a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py @@ -2,178 +2,6 @@ from sqlmodel import create_engine from sqlmodel.pool import StaticPool -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Offset", "type": "integer", "default": 0}, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "type": "integer", - "default": 100, - "lte": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": {"$ref": "#/components/schemas/HeroRead"}, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroCreate"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroRead"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroRead"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - }, - }, - "HeroRead": { - "title": "HeroRead", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "id": {"title": "Id", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"type": "string"}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - def test_tutorial(clear_sqlmodel): from docs_src.tutorial.fastapi.limit_and_offset import tutorial001 as mod @@ -207,10 +35,6 @@ def test_tutorial(clear_sqlmodel): assert response.status_code == 200, response.text response = client.get("/heroes/9000") assert response.status_code == 404, response.text - response = client.get("/openapi.json") - data = response.json() - assert response.status_code == 200, response.text - assert data == openapi_schema response = client.get("/heroes/") assert response.status_code == 200, response.text @@ -236,3 +60,192 @@ def test_tutorial(clear_sqlmodel): data = response.json() assert len(data) == 1 assert data[0]["name"] == hero2_data["name"] + + response = client.get("/openapi.json") + data = response.json() + assert response.status_code == 200, response.text + assert data == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Offset", + "type": "integer", + "default": 0, + }, + "name": "offset", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + "lte": 100, + }, + "name": "limit", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Heroes Heroes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/HeroRead" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/heroes/{hero_id}": { + "get": { + "summary": "Read Hero", + "operationId": "read_hero_heroes__hero_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "HeroCreate": { + "title": "HeroCreate", + "required": ["name", "secret_name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "HeroRead": { + "title": "HeroRead", + "required": ["name", "secret_name", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "id": {"title": "Id", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py index 125e00179c..2ea5e8d644 100644 --- a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py @@ -2,502 +2,6 @@ from sqlmodel import create_engine from sqlmodel.pool import StaticPool -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Offset", "type": "integer", "default": 0}, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "type": "integer", - "default": 100, - "lte": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": {"$ref": "#/components/schemas/HeroRead"}, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroCreate"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroRead"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroReadWithTeam" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "delete": { - "summary": "Delete Hero", - "operationId": "delete_hero_heroes__hero_id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Hero", - "operationId": "update_hero_heroes__hero_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroUpdate"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroRead"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/teams/": { - "get": { - "summary": "Read Teams", - "operationId": "read_teams_teams__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Offset", "type": "integer", "default": 0}, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "type": "integer", - "default": 100, - "lte": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Teams Teams Get", - "type": "array", - "items": {"$ref": "#/components/schemas/TeamRead"}, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Team", - "operationId": "create_team_teams__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/TeamCreate"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/TeamRead"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/teams/{team_id}": { - "get": { - "summary": "Read Team", - "operationId": "read_team_teams__team_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Team Id", "type": "integer"}, - "name": "team_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamReadWithHeroes" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "delete": { - "summary": "Delete Team", - "operationId": "delete_team_teams__team_id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Team Id", "type": "integer"}, - "name": "team_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Team", - "operationId": "update_team_teams__team_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Team Id", "type": "integer"}, - "name": "team_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/TeamUpdate"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/TeamRead"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "team_id": {"title": "Team Id", "type": "integer"}, - }, - }, - "HeroRead": { - "title": "HeroRead", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "team_id": {"title": "Team Id", "type": "integer"}, - "id": {"title": "Id", "type": "integer"}, - }, - }, - "HeroReadWithTeam": { - "title": "HeroReadWithTeam", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "team_id": {"title": "Team Id", "type": "integer"}, - "id": {"title": "Id", "type": "integer"}, - "team": {"$ref": "#/components/schemas/TeamRead"}, - }, - }, - "HeroUpdate": { - "title": "HeroUpdate", - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "team_id": {"title": "Team Id", "type": "integer"}, - }, - }, - "TeamCreate": { - "title": "TeamCreate", - "required": ["name", "headquarters"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "headquarters": {"title": "Headquarters", "type": "string"}, - }, - }, - "TeamRead": { - "title": "TeamRead", - "required": ["name", "headquarters", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "headquarters": {"title": "Headquarters", "type": "string"}, - "id": {"title": "Id", "type": "integer"}, - }, - }, - "TeamReadWithHeroes": { - "title": "TeamReadWithHeroes", - "required": ["name", "headquarters", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "headquarters": {"title": "Headquarters", "type": "string"}, - "id": {"title": "Id", "type": "integer"}, - "heroes": { - "title": "Heroes", - "type": "array", - "items": {"$ref": "#/components/schemas/HeroRead"}, - "default": [], - }, - }, - }, - "TeamUpdate": { - "title": "TeamUpdate", - "type": "object", - "properties": { - "id": {"title": "Id", "type": "integer"}, - "name": {"title": "Name", "type": "string"}, - "headquarters": {"title": "Headquarters", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"type": "string"}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - def test_tutorial(clear_sqlmodel): from docs_src.tutorial.fastapi.relationships import tutorial001 as mod @@ -508,11 +12,6 @@ def test_tutorial(clear_sqlmodel): ) with TestClient(mod.app) as client: - response = client.get("/openapi.json") - data = response.json() - assert response.status_code == 200, response.text - assert data == openapi_schema - team_preventers = {"name": "Preventers", "headquarters": "Sharp Tower"} team_z_force = {"name": "Z-Force", "headquarters": "Sister Margaret’s Bar"} response = client.post("/teams/", json=team_preventers) @@ -604,3 +103,532 @@ def test_tutorial(clear_sqlmodel): assert response.status_code == 200, response.text data = response.json() assert len(data) == 1 + + response = client.get("/openapi.json") + data = response.json() + assert response.status_code == 200, response.text + assert data == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Offset", + "type": "integer", + "default": 0, + }, + "name": "offset", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + "lte": 100, + }, + "name": "limit", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Heroes Heroes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/HeroRead" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/heroes/{hero_id}": { + "get": { + "summary": "Read Hero", + "operationId": "read_hero_heroes__hero_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroReadWithTeam" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "delete": { + "summary": "Delete Hero", + "operationId": "delete_hero_heroes__hero_id__delete", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "patch": { + "summary": "Update Hero", + "operationId": "update_hero_heroes__hero_id__patch", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroUpdate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/teams/": { + "get": { + "summary": "Read Teams", + "operationId": "read_teams_teams__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Offset", + "type": "integer", + "default": 0, + }, + "name": "offset", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + "lte": 100, + }, + "name": "limit", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Teams Teams Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/TeamRead" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "post": { + "summary": "Create Team", + "operationId": "create_team_teams__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/teams/{team_id}": { + "get": { + "summary": "Read Team", + "operationId": "read_team_teams__team_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Team Id", "type": "integer"}, + "name": "team_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamReadWithHeroes" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "delete": { + "summary": "Delete Team", + "operationId": "delete_team_teams__team_id__delete", + "parameters": [ + { + "required": True, + "schema": {"title": "Team Id", "type": "integer"}, + "name": "team_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "patch": { + "summary": "Update Team", + "operationId": "update_team_teams__team_id__patch", + "parameters": [ + { + "required": True, + "schema": {"title": "Team Id", "type": "integer"}, + "name": "team_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamUpdate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "HeroCreate": { + "title": "HeroCreate", + "required": ["name", "secret_name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "team_id": {"title": "Team Id", "type": "integer"}, + }, + }, + "HeroRead": { + "title": "HeroRead", + "required": ["name", "secret_name", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "team_id": {"title": "Team Id", "type": "integer"}, + "id": {"title": "Id", "type": "integer"}, + }, + }, + "HeroReadWithTeam": { + "title": "HeroReadWithTeam", + "required": ["name", "secret_name", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "team_id": {"title": "Team Id", "type": "integer"}, + "id": {"title": "Id", "type": "integer"}, + "team": {"$ref": "#/components/schemas/TeamRead"}, + }, + }, + "HeroUpdate": { + "title": "HeroUpdate", + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "team_id": {"title": "Team Id", "type": "integer"}, + }, + }, + "TeamCreate": { + "title": "TeamCreate", + "required": ["name", "headquarters"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "headquarters": {"title": "Headquarters", "type": "string"}, + }, + }, + "TeamRead": { + "title": "TeamRead", + "required": ["name", "headquarters", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "headquarters": {"title": "Headquarters", "type": "string"}, + "id": {"title": "Id", "type": "integer"}, + }, + }, + "TeamReadWithHeroes": { + "title": "TeamReadWithHeroes", + "required": ["name", "headquarters", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "headquarters": {"title": "Headquarters", "type": "string"}, + "id": {"title": "Id", "type": "integer"}, + "heroes": { + "title": "Heroes", + "type": "array", + "items": {"$ref": "#/components/schemas/HeroRead"}, + "default": [], + }, + }, + }, + "TeamUpdate": { + "title": "TeamUpdate", + "type": "object", + "properties": { + "id": {"title": "Id", "type": "integer"}, + "name": {"title": "Name", "type": "string"}, + "headquarters": {"title": "Headquarters", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py index 705146cc5b..da87b8e5c0 100644 --- a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py @@ -2,255 +2,6 @@ from sqlmodel import create_engine from sqlmodel.pool import StaticPool -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Offset", "type": "integer", "default": 0}, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "type": "integer", - "default": 100, - "lte": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": {"$ref": "#/components/schemas/HeroRead"}, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroCreate"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroRead"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroRead"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "delete": { - "summary": "Delete Hero", - "operationId": "delete_hero_heroes__hero_id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Hero", - "operationId": "update_hero_heroes__hero_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroUpdate"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroRead"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - }, - }, - "HeroRead": { - "title": "HeroRead", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "id": {"title": "Id", "type": "integer"}, - }, - }, - "HeroUpdate": { - "title": "HeroUpdate", - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"type": "string"}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - def test_tutorial(clear_sqlmodel): from docs_src.tutorial.fastapi.session_with_dependency import tutorial001 as mod @@ -284,10 +35,6 @@ def test_tutorial(clear_sqlmodel): assert response.status_code == 200, response.text response = client.get("/heroes/9000") assert response.status_code == 404, response.text - response = client.get("/openapi.json") - data = response.json() - assert response.status_code == 200, response.text - assert data == openapi_schema response = client.get("/heroes/") assert response.status_code == 200, response.text data = response.json() @@ -308,3 +55,273 @@ def test_tutorial(clear_sqlmodel): response = client.delete("/heroes/9000") assert response.status_code == 404, response.text + + response = client.get("/openapi.json") + data = response.json() + assert response.status_code == 200, response.text + assert data == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Offset", + "type": "integer", + "default": 0, + }, + "name": "offset", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + "lte": 100, + }, + "name": "limit", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Heroes Heroes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/HeroRead" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/heroes/{hero_id}": { + "get": { + "summary": "Read Hero", + "operationId": "read_hero_heroes__hero_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "delete": { + "summary": "Delete Hero", + "operationId": "delete_hero_heroes__hero_id__delete", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "patch": { + "summary": "Update Hero", + "operationId": "update_hero_heroes__hero_id__patch", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroUpdate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "HeroCreate": { + "title": "HeroCreate", + "required": ["name", "secret_name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "HeroRead": { + "title": "HeroRead", + "required": ["name", "secret_name", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "id": {"title": "Id", "type": "integer"}, + }, + }, + "HeroUpdate": { + "title": "HeroUpdate", + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py index 04a4b0c14d..08be835a17 100644 --- a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py @@ -2,468 +2,6 @@ from sqlmodel import create_engine from sqlmodel.pool import StaticPool -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Offset", "type": "integer", "default": 0}, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "type": "integer", - "default": 100, - "lte": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": {"$ref": "#/components/schemas/HeroRead"}, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroCreate"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroRead"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroRead"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "delete": { - "summary": "Delete Hero", - "operationId": "delete_hero_heroes__hero_id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Hero", - "operationId": "update_hero_heroes__hero_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroUpdate"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroRead"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/teams/": { - "get": { - "summary": "Read Teams", - "operationId": "read_teams_teams__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Offset", "type": "integer", "default": 0}, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "type": "integer", - "default": 100, - "lte": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Teams Teams Get", - "type": "array", - "items": {"$ref": "#/components/schemas/TeamRead"}, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Team", - "operationId": "create_team_teams__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/TeamCreate"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/TeamRead"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/teams/{team_id}": { - "get": { - "summary": "Read Team", - "operationId": "read_team_teams__team_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Team Id", "type": "integer"}, - "name": "team_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/TeamRead"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "delete": { - "summary": "Delete Team", - "operationId": "delete_team_teams__team_id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Team Id", "type": "integer"}, - "name": "team_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Team", - "operationId": "update_team_teams__team_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Team Id", "type": "integer"}, - "name": "team_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/TeamUpdate"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/TeamRead"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "team_id": {"title": "Team Id", "type": "integer"}, - }, - }, - "HeroRead": { - "title": "HeroRead", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "team_id": {"title": "Team Id", "type": "integer"}, - "id": {"title": "Id", "type": "integer"}, - }, - }, - "HeroUpdate": { - "title": "HeroUpdate", - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "team_id": {"title": "Team Id", "type": "integer"}, - }, - }, - "TeamCreate": { - "title": "TeamCreate", - "required": ["name", "headquarters"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "headquarters": {"title": "Headquarters", "type": "string"}, - }, - }, - "TeamRead": { - "title": "TeamRead", - "required": ["name", "headquarters", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "headquarters": {"title": "Headquarters", "type": "string"}, - "id": {"title": "Id", "type": "integer"}, - }, - }, - "TeamUpdate": { - "title": "TeamUpdate", - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "headquarters": {"title": "Headquarters", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"type": "string"}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - def test_tutorial(clear_sqlmodel): from docs_src.tutorial.fastapi.teams import tutorial001 as mod @@ -485,10 +23,6 @@ def test_tutorial(clear_sqlmodel): "secret_name": "Tommy Sharp", "age": 48, } - response = client.get("/openapi.json") - data = response.json() - assert response.status_code == 200, response.text - assert data == openapi_schema response = client.post("/heroes/", json=hero1_data) assert response.status_code == 200, response.text response = client.post("/heroes/", json=hero2_data) @@ -556,3 +90,502 @@ def test_tutorial(clear_sqlmodel): assert response.status_code == 200, response.text data = response.json() assert len(data) == 1 + + response = client.get("/openapi.json") + data = response.json() + assert response.status_code == 200, response.text + assert data == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Offset", + "type": "integer", + "default": 0, + }, + "name": "offset", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + "lte": 100, + }, + "name": "limit", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Heroes Heroes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/HeroRead" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/heroes/{hero_id}": { + "get": { + "summary": "Read Hero", + "operationId": "read_hero_heroes__hero_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "delete": { + "summary": "Delete Hero", + "operationId": "delete_hero_heroes__hero_id__delete", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "patch": { + "summary": "Update Hero", + "operationId": "update_hero_heroes__hero_id__patch", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroUpdate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/teams/": { + "get": { + "summary": "Read Teams", + "operationId": "read_teams_teams__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Offset", + "type": "integer", + "default": 0, + }, + "name": "offset", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + "lte": 100, + }, + "name": "limit", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Teams Teams Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/TeamRead" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "post": { + "summary": "Create Team", + "operationId": "create_team_teams__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/teams/{team_id}": { + "get": { + "summary": "Read Team", + "operationId": "read_team_teams__team_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Team Id", "type": "integer"}, + "name": "team_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "delete": { + "summary": "Delete Team", + "operationId": "delete_team_teams__team_id__delete", + "parameters": [ + { + "required": True, + "schema": {"title": "Team Id", "type": "integer"}, + "name": "team_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "patch": { + "summary": "Update Team", + "operationId": "update_team_teams__team_id__patch", + "parameters": [ + { + "required": True, + "schema": {"title": "Team Id", "type": "integer"}, + "name": "team_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamUpdate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "HeroCreate": { + "title": "HeroCreate", + "required": ["name", "secret_name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "team_id": {"title": "Team Id", "type": "integer"}, + }, + }, + "HeroRead": { + "title": "HeroRead", + "required": ["name", "secret_name", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "team_id": {"title": "Team Id", "type": "integer"}, + "id": {"title": "Id", "type": "integer"}, + }, + }, + "HeroUpdate": { + "title": "HeroUpdate", + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "team_id": {"title": "Team Id", "type": "integer"}, + }, + }, + "TeamCreate": { + "title": "TeamCreate", + "required": ["name", "headquarters"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "headquarters": {"title": "Headquarters", "type": "string"}, + }, + }, + "TeamRead": { + "title": "TeamRead", + "required": ["name", "headquarters", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "headquarters": {"title": "Headquarters", "type": "string"}, + "id": {"title": "Id", "type": "integer"}, + }, + }, + "TeamUpdate": { + "title": "TeamUpdate", + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "headquarters": {"title": "Headquarters", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py index ec75a2f25d..7f48780109 100644 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py @@ -2,227 +2,6 @@ from sqlmodel import create_engine from sqlmodel.pool import StaticPool -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Offset", "type": "integer", "default": 0}, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "type": "integer", - "default": 100, - "lte": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": {"$ref": "#/components/schemas/HeroRead"}, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroCreate"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroRead"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroRead"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Hero", - "operationId": "update_hero_heroes__hero_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroUpdate"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroRead"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - }, - }, - "HeroRead": { - "title": "HeroRead", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "id": {"title": "Id", "type": "integer"}, - }, - }, - "HeroUpdate": { - "title": "HeroUpdate", - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"type": "string"}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - def test_tutorial(clear_sqlmodel): from docs_src.tutorial.fastapi.update import tutorial001 as mod @@ -258,10 +37,6 @@ def test_tutorial(clear_sqlmodel): assert response.status_code == 200, response.text response = client.get("/heroes/9000") assert response.status_code == 404, response.text - response = client.get("/openapi.json") - data = response.json() - assert response.status_code == 200, response.text - assert data == openapi_schema response = client.get("/heroes/") assert response.status_code == 200, response.text data = response.json() @@ -287,3 +62,245 @@ def test_tutorial(clear_sqlmodel): response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) assert response.status_code == 404, response.text + + response = client.get("/openapi.json") + data = response.json() + assert response.status_code == 200, response.text + assert data == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Offset", + "type": "integer", + "default": 0, + }, + "name": "offset", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + "lte": 100, + }, + "name": "limit", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Heroes Heroes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/HeroRead" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/heroes/{hero_id}": { + "get": { + "summary": "Read Hero", + "operationId": "read_hero_heroes__hero_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "patch": { + "summary": "Update Hero", + "operationId": "update_hero_heroes__hero_id__patch", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroUpdate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "HeroCreate": { + "title": "HeroCreate", + "required": ["name", "secret_name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "HeroRead": { + "title": "HeroRead", + "required": ["name", "secret_name", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "id": {"title": "Id", "type": "integer"}, + }, + }, + "HeroUpdate": { + "title": "HeroUpdate", + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } From beb7a242758257f2cd6605a52bdcdab1ce9e34e5 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 23 Oct 2023 15:33:44 +0000 Subject: [PATCH 247/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 14d5e9c0f1..979b3a632a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✅ Refactor OpenAPI FastAPI tests to simplify updating them later, this moves things around without changes. PR [#671](https://github.com/tiangolo/sqlmodel/pull/671) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump actions/checkout from 3 to 4. PR [#670](https://github.com/tiangolo/sqlmodel/pull/670) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔧 Update mypy config, use `strict = true` instead of manual configs. PR [#428](https://github.com/tiangolo/sqlmodel/pull/428) by [@michaeloliverx](https://github.com/michaeloliverx). * 🐛 Fix `AsyncSession` type annotations for `exec()`. PR [#58](https://github.com/tiangolo/sqlmodel/pull/58) by [@Bobronium](https://github.com/Bobronium). From d192142eb9e83d52c3cb5bd7d5bcba93d40cb41c Mon Sep 17 00:00:00 2001 From: Jerry Wu Date: Tue, 24 Oct 2023 00:28:51 +0800 Subject: [PATCH 248/906] =?UTF-8?q?=F0=9F=93=9D=20Fix=20docs=20for=20Pydan?= =?UTF-8?q?tic's=20fields=20using=20`le`=20(`lte`=20is=20invalid,=20use=20?= =?UTF-8?q?`le`=20)=20(#207)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/tutorial/fastapi/limit-and-offset.md | 2 +- docs_src/tutorial/fastapi/app_testing/tutorial001/main.py | 2 +- docs_src/tutorial/fastapi/delete/tutorial001.py | 2 +- docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py | 2 +- docs_src/tutorial/fastapi/relationships/tutorial001.py | 4 ++-- .../fastapi/session_with_dependency/tutorial001.py | 2 +- docs_src/tutorial/fastapi/teams/tutorial001.py | 4 ++-- docs_src/tutorial/fastapi/update/tutorial001.py | 2 +- .../test_fastapi/test_delete/test_tutorial001.py | 5 ++--- .../test_fastapi/test_limit_and_offset/test_tutorial001.py | 5 ++--- .../test_fastapi/test_relationships/test_tutorial001.py | 7 +++---- .../test_session_with_dependency/test_tutorial001.py | 5 ++--- .../test_fastapi/test_teams/test_tutorial001.py | 7 +++---- .../test_fastapi/test_update/test_tutorial001.py | 5 ++--- 14 files changed, 24 insertions(+), 30 deletions(-) diff --git a/docs/tutorial/fastapi/limit-and-offset.md b/docs/tutorial/fastapi/limit-and-offset.md index 92bbfc7ee0..8802f4ec99 100644 --- a/docs/tutorial/fastapi/limit-and-offset.md +++ b/docs/tutorial/fastapi/limit-and-offset.md @@ -42,7 +42,7 @@ We want to allow clients to set different `offset` and `limit` values. But we don't want them to be able to set a `limit` of something like `9999`, that's over `9000`! 😱 -So, to prevent it, we add additional validation to the `limit` query parameter, declaring that it has to be **l**ess **t**han or **e**qual to `100` with `lte=100`. +So, to prevent it, we add additional validation to the `limit` query parameter, declaring that it has to be **l**ess than or **e**qual to `100` with `le=100`. This way, a client can decide to take fewer heroes if they want, but not more. diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py b/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py index d106c4ebab..3f0602e4b4 100644 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py @@ -66,7 +66,7 @@ def read_heroes( *, session: Session = Depends(get_session), offset: int = 0, - limit: int = Query(default=100, lte=100), + limit: int = Query(default=100, le=100), ): heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() return heroes diff --git a/docs_src/tutorial/fastapi/delete/tutorial001.py b/docs_src/tutorial/fastapi/delete/tutorial001.py index 3c15efbb2d..3069fc5e87 100644 --- a/docs_src/tutorial/fastapi/delete/tutorial001.py +++ b/docs_src/tutorial/fastapi/delete/tutorial001.py @@ -58,7 +58,7 @@ def create_hero(hero: HeroCreate): @app.get("/heroes/", response_model=List[HeroRead]) -def read_heroes(offset: int = 0, limit: int = Query(default=100, lte=100)): +def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): with Session(engine) as session: heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() return heroes diff --git a/docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py b/docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py index aef21332a7..2b8739ca70 100644 --- a/docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py +++ b/docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py @@ -52,7 +52,7 @@ def create_hero(hero: HeroCreate): @app.get("/heroes/", response_model=List[HeroRead]) -def read_heroes(offset: int = 0, limit: int = Query(default=100, lte=100)): +def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): with Session(engine) as session: heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() return heroes diff --git a/docs_src/tutorial/fastapi/relationships/tutorial001.py b/docs_src/tutorial/fastapi/relationships/tutorial001.py index 6a03846686..8477e4a2a0 100644 --- a/docs_src/tutorial/fastapi/relationships/tutorial001.py +++ b/docs_src/tutorial/fastapi/relationships/tutorial001.py @@ -104,7 +104,7 @@ def read_heroes( *, session: Session = Depends(get_session), offset: int = 0, - limit: int = Query(default=100, lte=100), + limit: int = Query(default=100, le=100), ): heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() return heroes @@ -158,7 +158,7 @@ def read_teams( *, session: Session = Depends(get_session), offset: int = 0, - limit: int = Query(default=100, lte=100), + limit: int = Query(default=100, le=100), ): teams = session.exec(select(Team).offset(offset).limit(limit)).all() return teams diff --git a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py index d106c4ebab..3f0602e4b4 100644 --- a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py +++ b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py @@ -66,7 +66,7 @@ def read_heroes( *, session: Session = Depends(get_session), offset: int = 0, - limit: int = Query(default=100, lte=100), + limit: int = Query(default=100, le=100), ): heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() return heroes diff --git a/docs_src/tutorial/fastapi/teams/tutorial001.py b/docs_src/tutorial/fastapi/teams/tutorial001.py index 6f84182bc9..1da0dad8a2 100644 --- a/docs_src/tutorial/fastapi/teams/tutorial001.py +++ b/docs_src/tutorial/fastapi/teams/tutorial001.py @@ -95,7 +95,7 @@ def read_heroes( *, session: Session = Depends(get_session), offset: int = 0, - limit: int = Query(default=100, lte=100), + limit: int = Query(default=100, le=100), ): heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() return heroes @@ -149,7 +149,7 @@ def read_teams( *, session: Session = Depends(get_session), offset: int = 0, - limit: int = Query(default=100, lte=100), + limit: int = Query(default=100, le=100), ): teams = session.exec(select(Team).offset(offset).limit(limit)).all() return teams diff --git a/docs_src/tutorial/fastapi/update/tutorial001.py b/docs_src/tutorial/fastapi/update/tutorial001.py index 35554878db..bb98efd581 100644 --- a/docs_src/tutorial/fastapi/update/tutorial001.py +++ b/docs_src/tutorial/fastapi/update/tutorial001.py @@ -58,7 +58,7 @@ def create_hero(hero: HeroCreate): @app.get("/heroes/", response_model=List[HeroRead]) -def read_heroes(offset: int = 0, limit: int = Query(default=100, lte=100)): +def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): with Session(engine) as session: heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() return heroes diff --git a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py index 8979245dd6..b08affb920 100644 --- a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py @@ -57,9 +57,8 @@ def test_tutorial(clear_sqlmodel): assert response.status_code == 404, response.text response = client.get("/openapi.json") - data = response.json() assert response.status_code == 200, response.text - assert data == { + assert response.json() == { "openapi": "3.0.2", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { @@ -82,9 +81,9 @@ def test_tutorial(clear_sqlmodel): "required": False, "schema": { "title": "Limit", + "maximum": 100.0, "type": "integer", "default": 100, - "lte": 100, }, "name": "limit", "in": "query", diff --git a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py index d7f642a788..0aee3ca004 100644 --- a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py @@ -62,9 +62,8 @@ def test_tutorial(clear_sqlmodel): assert data[0]["name"] == hero2_data["name"] response = client.get("/openapi.json") - data = response.json() assert response.status_code == 200, response.text - assert data == { + assert response.json() == { "openapi": "3.0.2", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { @@ -87,9 +86,9 @@ def test_tutorial(clear_sqlmodel): "required": False, "schema": { "title": "Limit", + "maximum": 100.0, "type": "integer", "default": 100, - "lte": 100, }, "name": "limit", "in": "query", diff --git a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py index 2ea5e8d644..8869862e95 100644 --- a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py @@ -105,9 +105,8 @@ def test_tutorial(clear_sqlmodel): assert len(data) == 1 response = client.get("/openapi.json") - data = response.json() assert response.status_code == 200, response.text - assert data == { + assert response.json() == { "openapi": "3.0.2", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { @@ -130,9 +129,9 @@ def test_tutorial(clear_sqlmodel): "required": False, "schema": { "title": "Limit", + "maximum": 100.0, "type": "integer", "default": 100, - "lte": 100, }, "name": "limit", "in": "query", @@ -329,9 +328,9 @@ def test_tutorial(clear_sqlmodel): "required": False, "schema": { "title": "Limit", + "maximum": 100.0, "type": "integer", "default": 100, - "lte": 100, }, "name": "limit", "in": "query", diff --git a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py index da87b8e5c0..cb0a6f9282 100644 --- a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py @@ -57,9 +57,8 @@ def test_tutorial(clear_sqlmodel): assert response.status_code == 404, response.text response = client.get("/openapi.json") - data = response.json() assert response.status_code == 200, response.text - assert data == { + assert response.json() == { "openapi": "3.0.2", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { @@ -82,9 +81,9 @@ def test_tutorial(clear_sqlmodel): "required": False, "schema": { "title": "Limit", + "maximum": 100.0, "type": "integer", "default": 100, - "lte": 100, }, "name": "limit", "in": "query", diff --git a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py index 08be835a17..e66c975142 100644 --- a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py @@ -92,9 +92,8 @@ def test_tutorial(clear_sqlmodel): assert len(data) == 1 response = client.get("/openapi.json") - data = response.json() assert response.status_code == 200, response.text - assert data == { + assert response.json() == { "openapi": "3.0.2", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { @@ -117,9 +116,9 @@ def test_tutorial(clear_sqlmodel): "required": False, "schema": { "title": "Limit", + "maximum": 100.0, "type": "integer", "default": 100, - "lte": 100, }, "name": "limit", "in": "query", @@ -316,9 +315,9 @@ def test_tutorial(clear_sqlmodel): "required": False, "schema": { "title": "Limit", + "maximum": 100.0, "type": "integer", "default": 100, - "lte": 100, }, "name": "limit", "in": "query", diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py index 7f48780109..49906256c9 100644 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py @@ -64,9 +64,8 @@ def test_tutorial(clear_sqlmodel): assert response.status_code == 404, response.text response = client.get("/openapi.json") - data = response.json() assert response.status_code == 200, response.text - assert data == { + assert response.json() == { "openapi": "3.0.2", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { @@ -89,9 +88,9 @@ def test_tutorial(clear_sqlmodel): "required": False, "schema": { "title": "Limit", + "maximum": 100.0, "type": "integer", "default": 100, - "lte": 100, }, "name": "limit", "in": "query", From a1caaa08d743700e0185996e6d203f72e61e949d Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 23 Oct 2023 16:29:36 +0000 Subject: [PATCH 249/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 979b3a632a..2283b212fa 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Fix docs for Pydantic's fields using `le` (`lte` is invalid, use `le` ). PR [#207](https://github.com/tiangolo/sqlmodel/pull/207) by [@jrycw](https://github.com/jrycw). * ✅ Refactor OpenAPI FastAPI tests to simplify updating them later, this moves things around without changes. PR [#671](https://github.com/tiangolo/sqlmodel/pull/671) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump actions/checkout from 3 to 4. PR [#670](https://github.com/tiangolo/sqlmodel/pull/670) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔧 Update mypy config, use `strict = true` instead of manual configs. PR [#428](https://github.com/tiangolo/sqlmodel/pull/428) by [@michaeloliverx](https://github.com/michaeloliverx). From 403d44ea780cc59fadba2d485837685b4764c07a Mon Sep 17 00:00:00 2001 From: Jon Michaelchuck <5964742+jbmchuck@users.noreply.github.com> Date: Mon, 23 Oct 2023 09:55:53 -0700 Subject: [PATCH 250/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20docs,=20use=20`?= =?UTF-8?q?offset`=20in=20example=20with=20`limit`=20and=20`where`=20(#273?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/tutorial/limit-and-offset.md | 15 +++++++-------- .../tutorial/offset_and_limit/tutorial004.py | 2 +- .../test_limit_and_offset/test_tutorial004.py | 19 ++++++++----------- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/docs/tutorial/limit-and-offset.md b/docs/tutorial/limit-and-offset.md index dc4c28063c..c8b0ddf72f 100644 --- a/docs/tutorial/limit-and-offset.md +++ b/docs/tutorial/limit-and-offset.md @@ -271,11 +271,11 @@ Of course, you can also combine `.limit()` and `.offset()` with `.where()` and o
-## Run the Program with Limit and Where on the Command Line +## Run the Program with Limit, Offset, and Where on the Command Line If we run it on the command line, it will find all the heroes in the database with an age above 32. That would normally be 4 heroes. -But we are limiting the results to only get the first 3: +But we are starting to include after an offset of 1 (so we don't count the first one), and we are limiting the results to only get the first 2 after that:
@@ -284,18 +284,17 @@ $ python app.py // Previous output omitted 🙈 -// Select with WHERE and LIMIT +// Select with WHERE and LIMIT and OFFSET INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age FROM hero WHERE hero.age > ? LIMIT ? OFFSET ? -INFO Engine [no key 0.00022s] (32, 3, 0) +INFO Engine [no key 0.00022s] (32, 2, 1) -// Print the heroes received, only 3 +// Print the heroes received, only 2 [ - Hero(age=35, secret_name='Trevor Challa', id=5, name='Black Lion'), - Hero(age=36, secret_name='Steve Weird', id=6, name='Dr. Weird'), - Hero(age=48, secret_name='Tommy Sharp', id=3, name='Rusty-Man') + Hero(age=36, id=6, name='Dr. Weird', secret_name='Steve Weird'), + Hero(age=48, id=3, name='Rusty-Man', secret_name='Tommy Sharp') ] ``` diff --git a/docs_src/tutorial/offset_and_limit/tutorial004.py b/docs_src/tutorial/offset_and_limit/tutorial004.py index a95715cd98..43828b853f 100644 --- a/docs_src/tutorial/offset_and_limit/tutorial004.py +++ b/docs_src/tutorial/offset_and_limit/tutorial004.py @@ -43,7 +43,7 @@ def create_heroes(): def select_heroes(): with Session(engine) as session: - statement = select(Hero).where(Hero.age > 32).limit(3) + statement = select(Hero).where(Hero.age > 32).offset(1).limit(2) results = session.exec(statement) heroes = results.all() print(heroes) diff --git a/tests/test_tutorial/test_limit_and_offset/test_tutorial004.py b/tests/test_tutorial/test_limit_and_offset/test_tutorial004.py index 448b191249..eb15a1560e 100644 --- a/tests/test_tutorial/test_limit_and_offset/test_tutorial004.py +++ b/tests/test_tutorial/test_limit_and_offset/test_tutorial004.py @@ -4,16 +4,6 @@ from ...conftest import get_testing_print_function -expected_calls = [ - [ - [ - {"id": 5, "name": "Black Lion", "secret_name": "Trevor Challa", "age": 35}, - {"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}, - {"id": 3, "name": "Rusty-Man", "secret_name": "Tommy Sharp", "age": 48}, - ] - ] -] - def test_tutorial(clear_sqlmodel): from docs_src.tutorial.offset_and_limit import tutorial004 as mod @@ -26,4 +16,11 @@ def test_tutorial(clear_sqlmodel): with patch("builtins.print", new=new_print): mod.main() - assert calls == expected_calls + assert calls == [ + [ + [ + {"name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36, "id": 6}, + {"name": "Rusty-Man", "secret_name": "Tommy Sharp", "age": 48, "id": 3}, + ] + ] + ] From 03f295b3976979922caba569a7ac21234d2b4a48 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 23 Oct 2023 16:56:29 +0000 Subject: [PATCH 251/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 2283b212fa..7fcb59aff3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Update docs, use `offset` in example with `limit` and `where`. PR [#273](https://github.com/tiangolo/sqlmodel/pull/273) by [@jbmchuck](https://github.com/jbmchuck). * 📝 Fix docs for Pydantic's fields using `le` (`lte` is invalid, use `le` ). PR [#207](https://github.com/tiangolo/sqlmodel/pull/207) by [@jrycw](https://github.com/jrycw). * ✅ Refactor OpenAPI FastAPI tests to simplify updating them later, this moves things around without changes. PR [#671](https://github.com/tiangolo/sqlmodel/pull/671) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump actions/checkout from 3 to 4. PR [#670](https://github.com/tiangolo/sqlmodel/pull/670) by [@dependabot[bot]](https://github.com/apps/dependabot). From 6d361e3ffb542cdeef34127fd555494a300cc318 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 21:05:55 +0400 Subject: [PATCH 252/906] =?UTF-8?q?=E2=AC=86=20Bump=20dawidd6/action-downl?= =?UTF-8?q?oad-artifact=20from=202.24.2=20to=202.28.0=20(#660)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 2.24.2 to 2.28.0. - [Release notes](https://github.com/dawidd6/action-download-artifact/releases) - [Commits](https://github.com/dawidd6/action-download-artifact/compare/v2.24.2...v2.28.0) --- updated-dependencies: - dependency-name: dawidd6/action-download-artifact dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/deploy-docs.yml | 2 +- .github/workflows/smokeshow.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index e7641a3c0f..f9035d89a7 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -21,7 +21,7 @@ jobs: mkdir ./site - name: Download Artifact Docs id: download - uses: dawidd6/action-download-artifact@v2.27.0 + uses: dawidd6/action-download-artifact@v2.28.0 with: if_no_artifact_found: ignore github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index 7ee17cac55..d2c274ff29 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -20,7 +20,7 @@ jobs: - run: pip install smokeshow - - uses: dawidd6/action-download-artifact@v2.24.2 + - uses: dawidd6/action-download-artifact@v2.28.0 with: workflow: test.yml commit: ${{ github.event.workflow_run.head_sha }} From fc3120a877c81298658d6e549f6fc14c3fdada00 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 23 Oct 2023 17:06:30 +0000 Subject: [PATCH 253/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 7fcb59aff3..161c6caf66 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump dawidd6/action-download-artifact from 2.24.2 to 2.28.0. PR [#660](https://github.com/tiangolo/sqlmodel/pull/660) by [@dependabot[bot]](https://github.com/apps/dependabot). * 📝 Update docs, use `offset` in example with `limit` and `where`. PR [#273](https://github.com/tiangolo/sqlmodel/pull/273) by [@jbmchuck](https://github.com/jbmchuck). * 📝 Fix docs for Pydantic's fields using `le` (`lte` is invalid, use `le` ). PR [#207](https://github.com/tiangolo/sqlmodel/pull/207) by [@jrycw](https://github.com/jrycw). * ✅ Refactor OpenAPI FastAPI tests to simplify updating them later, this moves things around without changes. PR [#671](https://github.com/tiangolo/sqlmodel/pull/671) by [@tiangolo](https://github.com/tiangolo). From 9b186c89a8a54812ba0c86c99b4e8768e5b79cd5 Mon Sep 17 00:00:00 2001 From: Abenezer Belachew <43050633+abenezerBelachew@users.noreply.github.com> Date: Mon, 23 Oct 2023 11:22:22 -0600 Subject: [PATCH 254/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20link=20to=20doc?= =?UTF-8?q?s=20for=20intro=20to=20databases=20(#593)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index a9387c510a..524ef992a3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -68,7 +68,7 @@ Successfully installed sqlmodel ## Example -For an introduction to databases, SQL, and everything else, see the SQLModel documentation. +For an introduction to databases, SQL, and everything else, see the SQLModel documentation. Here's a quick example. ✨ From 30d2b30217289b7828af9124ea205cd366321416 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 23 Oct 2023 17:22:58 +0000 Subject: [PATCH 255/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 161c6caf66..adb290aac4 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Update link to docs for intro to databases. PR [#593](https://github.com/tiangolo/sqlmodel/pull/593) by [@abenezerBelachew](https://github.com/abenezerBelachew). * ⬆ Bump dawidd6/action-download-artifact from 2.24.2 to 2.28.0. PR [#660](https://github.com/tiangolo/sqlmodel/pull/660) by [@dependabot[bot]](https://github.com/apps/dependabot). * 📝 Update docs, use `offset` in example with `limit` and `where`. PR [#273](https://github.com/tiangolo/sqlmodel/pull/273) by [@jbmchuck](https://github.com/jbmchuck). * 📝 Fix docs for Pydantic's fields using `le` (`lte` is invalid, use `le` ). PR [#207](https://github.com/tiangolo/sqlmodel/pull/207) by [@jrycw](https://github.com/jrycw). From 376603efb2799f5b0565a7a29660601ed629d1fd Mon Sep 17 00:00:00 2001 From: PookieBuns <83051474+PookieBuns@users.noreply.github.com> Date: Mon, 23 Oct 2023 12:21:38 -0700 Subject: [PATCH 256/906] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20contributing?= =?UTF-8?q?=20instructions=20to=20run=20tests,=20update=20script=20name=20?= =?UTF-8?q?(#634)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/contributing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contributing.md b/docs/contributing.md index 1cd62d42dc..217ed61c56 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -112,7 +112,7 @@ There is a script that you can run locally to test all the code and generate cov
```console -$ bash scripts/test-cov-html.sh +$ bash scripts/test.sh ```
From 6cd086f25fcf056b7f2a8263690115caeb05a8dc Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 23 Oct 2023 19:22:23 +0000 Subject: [PATCH 257/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index adb290aac4..510dbd1f74 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏️ Fix contributing instructions to run tests, update script name. PR [#634](https://github.com/tiangolo/sqlmodel/pull/634) by [@PookieBuns](https://github.com/PookieBuns). * 📝 Update link to docs for intro to databases. PR [#593](https://github.com/tiangolo/sqlmodel/pull/593) by [@abenezerBelachew](https://github.com/abenezerBelachew). * ⬆ Bump dawidd6/action-download-artifact from 2.24.2 to 2.28.0. PR [#660](https://github.com/tiangolo/sqlmodel/pull/660) by [@dependabot[bot]](https://github.com/apps/dependabot). * 📝 Update docs, use `offset` in example with `limit` and `where`. PR [#273](https://github.com/tiangolo/sqlmodel/pull/273) by [@jbmchuck](https://github.com/jbmchuck). From 9d3ca01dd09d77f4bcff000621f683936d9fe3ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 24 Oct 2023 00:37:07 +0400 Subject: [PATCH 258/906] =?UTF-8?q?=F0=9F=93=9D=20Tweak=20wording=20in=20`?= =?UTF-8?q?docs/tutorial/fastapi/multiple-models.md`=20(#674)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Luis Benitez --- docs/tutorial/fastapi/multiple-models.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/tutorial/fastapi/multiple-models.md b/docs/tutorial/fastapi/multiple-models.md index c37fad386b..6845b9862d 100644 --- a/docs/tutorial/fastapi/multiple-models.md +++ b/docs/tutorial/fastapi/multiple-models.md @@ -53,11 +53,11 @@ Here's the weird thing, the `id` currently seems also "optional". 🤔 This is because in our **SQLModel** class we declare the `id` with `Optional[int]`, because it could be `None` in memory until we save it in the database and we finally get the actual ID. -But in the responses, we would always send a model from the database, and it would **always have an ID**. So the `id` in the responses could be declared as required too. +But in the responses, we always send a model from the database, so it **always has an ID**. So the `id` in the responses can be declared as required. -This would mean that our application is making the compromise with the clients that if it sends a hero, it would for sure have an `id` with a value, it would not be `None`. +This means that our application is making the promise to the clients that if it sends a hero, it will for sure have an `id` with a value, it will not be `None`. -### Why Is it Important to Compromise with the Responses +### Why Is it Important to Have a Contract for Responses The ultimate goal of an API is for some **clients to use it**. From 9fd73066488d1ed158104e6a91f4f7181ec45898 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 23 Oct 2023 20:37:48 +0000 Subject: [PATCH 259/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 510dbd1f74..6cd3a0869d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Tweak wording in `docs/tutorial/fastapi/multiple-models.md`. PR [#674](https://github.com/tiangolo/sqlmodel/pull/674) by [@tiangolo](https://github.com/tiangolo). * ✏️ Fix contributing instructions to run tests, update script name. PR [#634](https://github.com/tiangolo/sqlmodel/pull/634) by [@PookieBuns](https://github.com/PookieBuns). * 📝 Update link to docs for intro to databases. PR [#593](https://github.com/tiangolo/sqlmodel/pull/593) by [@abenezerBelachew](https://github.com/abenezerBelachew). * ⬆ Bump dawidd6/action-download-artifact from 2.24.2 to 2.28.0. PR [#660](https://github.com/tiangolo/sqlmodel/pull/660) by [@dependabot[bot]](https://github.com/apps/dependabot). From 1acb683f80fcf5eae1191b68f12dd398f34620a8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 24 Oct 2023 00:45:45 +0400 Subject: [PATCH 260/906] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#672)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.4.0 → v4.5.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.4.0...v4.5.0) - [github.com/asottile/pyupgrade: v3.7.0 → v3.15.0](https://github.com/asottile/pyupgrade/compare/v3.7.0...v3.15.0) - https://github.com/charliermarsh/ruff-pre-commit → https://github.com/astral-sh/ruff-pre-commit - [github.com/astral-sh/ruff-pre-commit: v0.0.275 → v0.1.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.275...v0.1.1) - [github.com/psf/black: 23.3.0 → 23.10.0](https://github.com/psf/black/compare/23.3.0...23.10.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9f7085f72f..022ef24a0f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ default_language_version: python: python3.10 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: check-added-large-files - id: check-toml @@ -14,20 +14,20 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/asottile/pyupgrade - rev: v3.7.0 + rev: v3.15.0 hooks: - id: pyupgrade args: - --py3-plus - --keep-runtime-typing -- repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.275 +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.1 hooks: - id: ruff args: - --fix - repo: https://github.com/psf/black - rev: 23.3.0 + rev: 23.10.0 hooks: - id: black ci: From 80fd7e03cf361e49b473057df815e953e4653e20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Wei=C3=9F?= Date: Mon, 23 Oct 2023 22:46:05 +0200 Subject: [PATCH 261/906] =?UTF-8?q?=F0=9F=93=9D=20Clarify=20description=20?= =?UTF-8?q?of=20in-memory=20SQLite=20database=20in=20`docs/tutorial/create?= =?UTF-8?q?-db-and-table.md`=20(#601)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/tutorial/create-db-and-table.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/create-db-and-table.md b/docs/tutorial/create-db-and-table.md index 52a12fa9c3..d0d27424d1 100644 --- a/docs/tutorial/create-db-and-table.md +++ b/docs/tutorial/create-db-and-table.md @@ -220,7 +220,7 @@ Each supported database has it's own URL type. For example, for **SQLite** it is * `sqlite:///databases/local/application.db` * `sqlite:///db.sqlite` -For SQLAlchemy, there's also a special one, which is a database all *in memory*, this means that it is deleted after the program terminates, and it's also very fast: +SQLite supports a special database that lives all *in memory*. Hence, it's very fast, but be careful, the database gets deleted after the program terminates. You can specify this in-memory database by using just two slash characters (`//`) and no file name: * `sqlite://` From dcc4e4c36ae76477edfa6411734953c566862a63 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 23 Oct 2023 20:46:26 +0000 Subject: [PATCH 262/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 6cd3a0869d..6a2d45fbe1 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#672](https://github.com/tiangolo/sqlmodel/pull/672) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * 📝 Tweak wording in `docs/tutorial/fastapi/multiple-models.md`. PR [#674](https://github.com/tiangolo/sqlmodel/pull/674) by [@tiangolo](https://github.com/tiangolo). * ✏️ Fix contributing instructions to run tests, update script name. PR [#634](https://github.com/tiangolo/sqlmodel/pull/634) by [@PookieBuns](https://github.com/PookieBuns). * 📝 Update link to docs for intro to databases. PR [#593](https://github.com/tiangolo/sqlmodel/pull/593) by [@abenezerBelachew](https://github.com/abenezerBelachew). From a6ce817ca5e6b70c9427d89194dde07bb6c73cb8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 23 Oct 2023 20:46:39 +0000 Subject: [PATCH 263/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 6a2d45fbe1..09427d809d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Clarify description of in-memory SQLite database in `docs/tutorial/create-db-and-table.md`. PR [#601](https://github.com/tiangolo/sqlmodel/pull/601) by [@SimonCW](https://github.com/SimonCW). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#672](https://github.com/tiangolo/sqlmodel/pull/672) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * 📝 Tweak wording in `docs/tutorial/fastapi/multiple-models.md`. PR [#674](https://github.com/tiangolo/sqlmodel/pull/674) by [@tiangolo](https://github.com/tiangolo). * ✏️ Fix contributing instructions to run tests, update script name. PR [#634](https://github.com/tiangolo/sqlmodel/pull/634) by [@PookieBuns](https://github.com/PookieBuns). From 596718d93b385c185693827145c4cf3ecb3004ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 24 Oct 2023 00:59:49 +0400 Subject: [PATCH 264/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 57 ++++++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 09427d809d..9e95ad3cab 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,50 +2,67 @@ ## Latest Changes +### Breaking Changes + +* 🗑️ Deprecate Python 3.6 and upgrade Poetry and Poetry Version Plugin. PR [#627](https://github.com/tiangolo/sqlmodel/pull/627) by [@tiangolo](https://github.com/tiangolo). + +### Features + +* ✨ Raise a more clear error when a type is not valid. PR [#425](https://github.com/tiangolo/sqlmodel/pull/425) by [@ddanier](https://github.com/ddanier). + +### Fixes + +* 🐛 Fix `AsyncSession` type annotations for `exec()`. PR [#58](https://github.com/tiangolo/sqlmodel/pull/58) by [@Bobronium](https://github.com/Bobronium). +* 🐛 Fix allowing using a `ForeignKey` directly, remove repeated column construction from `SQLModelMetaclass.__init__` and upgrade minimum SQLAlchemy to `>=1.4.36`. PR [#443](https://github.com/tiangolo/sqlmodel/pull/443) by [@daniil-berg](https://github.com/daniil-berg). +* 🐛 Fix enum type checks ordering in `get_sqlalchemy_type`. PR [#669](https://github.com/tiangolo/sqlmodel/pull/669) by [@tiangolo](https://github.com/tiangolo). +* 🐛 Fix SQLAlchemy version 1.4.36 breaks SQLModel relationships (#315). PR [#461](https://github.com/tiangolo/sqlmodel/pull/461) by [@byrman](https://github.com/byrman). + +### Upgrades + +* ⬆️ Upgrade support for SQLAlchemy 1.4.49, update tests. PR [#519](https://github.com/tiangolo/sqlmodel/pull/519) by [@sandrotosi](https://github.com/sandrotosi). +* ⬆ Raise SQLAlchemy version requirement to at least `1.4.29` (related to #434). PR [#439](https://github.com/tiangolo/sqlmodel/pull/439) by [@daniil-berg](https://github.com/daniil-berg). + +### Docs + * 📝 Clarify description of in-memory SQLite database in `docs/tutorial/create-db-and-table.md`. PR [#601](https://github.com/tiangolo/sqlmodel/pull/601) by [@SimonCW](https://github.com/SimonCW). -* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#672](https://github.com/tiangolo/sqlmodel/pull/672) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * 📝 Tweak wording in `docs/tutorial/fastapi/multiple-models.md`. PR [#674](https://github.com/tiangolo/sqlmodel/pull/674) by [@tiangolo](https://github.com/tiangolo). * ✏️ Fix contributing instructions to run tests, update script name. PR [#634](https://github.com/tiangolo/sqlmodel/pull/634) by [@PookieBuns](https://github.com/PookieBuns). * 📝 Update link to docs for intro to databases. PR [#593](https://github.com/tiangolo/sqlmodel/pull/593) by [@abenezerBelachew](https://github.com/abenezerBelachew). -* ⬆ Bump dawidd6/action-download-artifact from 2.24.2 to 2.28.0. PR [#660](https://github.com/tiangolo/sqlmodel/pull/660) by [@dependabot[bot]](https://github.com/apps/dependabot). * 📝 Update docs, use `offset` in example with `limit` and `where`. PR [#273](https://github.com/tiangolo/sqlmodel/pull/273) by [@jbmchuck](https://github.com/jbmchuck). * 📝 Fix docs for Pydantic's fields using `le` (`lte` is invalid, use `le` ). PR [#207](https://github.com/tiangolo/sqlmodel/pull/207) by [@jrycw](https://github.com/jrycw). +* 📝 Update outdated link in `docs/db-to-code.md`. PR [#649](https://github.com/tiangolo/sqlmodel/pull/649) by [@MatveyF](https://github.com/MatveyF). +* ✏️ Fix typos found with codespell. PR [#520](https://github.com/tiangolo/sqlmodel/pull/520) by [@kianmeng](https://github.com/kianmeng). +* 📝 Fix typos (duplication) in main page. PR [#631](https://github.com/tiangolo/sqlmodel/pull/631) by [@Mr-DRP](https://github.com/Mr-DRP). +* 📝 Update release notes, add second author to PR. PR [#429](https://github.com/tiangolo/sqlmodel/pull/429) by [@br-follow](https://github.com/br-follow). +* 📝 Update instructions about how to make a foreign key required in `docs/tutorial/relationship-attributes/define-relationships-attributes.md`. PR [#474](https://github.com/tiangolo/sqlmodel/pull/474) by [@jalvaradosegura](https://github.com/jalvaradosegura). +* 📝 Update help SQLModel docs. PR [#548](https://github.com/tiangolo/sqlmodel/pull/548) by [@tiangolo](https://github.com/tiangolo). +* ✏️ Fix typo in internal function name `get_sqlachemy_type()`. PR [#496](https://github.com/tiangolo/sqlmodel/pull/496) by [@cmarqu](https://github.com/cmarqu). +* ✏️ Fix typo in docs. PR [#446](https://github.com/tiangolo/sqlmodel/pull/446) by [@davidbrochart](https://github.com/davidbrochart). +* ✏️ Fix typo in `docs/tutorial/create-db-and-table.md`. PR [#477](https://github.com/tiangolo/sqlmodel/pull/477) by [@FluffyDietEngine](https://github.com/FluffyDietEngine). +* ✏️ Fix small typos in docs. PR [#481](https://github.com/tiangolo/sqlmodel/pull/481) by [@micuffaro](https://github.com/micuffaro). + +### Internal + +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#672](https://github.com/tiangolo/sqlmodel/pull/672) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump dawidd6/action-download-artifact from 2.24.2 to 2.28.0. PR [#660](https://github.com/tiangolo/sqlmodel/pull/660) by [@dependabot[bot]](https://github.com/apps/dependabot). * ✅ Refactor OpenAPI FastAPI tests to simplify updating them later, this moves things around without changes. PR [#671](https://github.com/tiangolo/sqlmodel/pull/671) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump actions/checkout from 3 to 4. PR [#670](https://github.com/tiangolo/sqlmodel/pull/670) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔧 Update mypy config, use `strict = true` instead of manual configs. PR [#428](https://github.com/tiangolo/sqlmodel/pull/428) by [@michaeloliverx](https://github.com/michaeloliverx). -* 🐛 Fix `AsyncSession` type annotations for `exec()`. PR [#58](https://github.com/tiangolo/sqlmodel/pull/58) by [@Bobronium](https://github.com/Bobronium). -* 🐛 Fix allowing using a `ForeignKey` directly, remove repeated column construction from `SQLModelMetaclass.__init__` and upgrade minimum SQLAlchemy to `>=1.4.36`. PR [#443](https://github.com/tiangolo/sqlmodel/pull/443) by [@daniil-berg](https://github.com/daniil-berg). -* ⬆️ Upgrade support for SQLAlchemy 1.4.49, update tests. PR [#519](https://github.com/tiangolo/sqlmodel/pull/519) by [@sandrotosi](https://github.com/sandrotosi). -* ⬆ Raise SQLAlchemy version requirement to at least `1.4.29` (related to #434). PR [#439](https://github.com/tiangolo/sqlmodel/pull/439) by [@daniil-berg](https://github.com/daniil-berg). -* 🐛 Fix enum type checks ordering in `get_sqlalchemy_type`. PR [#669](https://github.com/tiangolo/sqlmodel/pull/669) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade MkDocs Material. PR [#668](https://github.com/tiangolo/sqlmodel/pull/668) by [@tiangolo](https://github.com/tiangolo). * 🎨 Update docs format and references with pre-commit and Ruff. PR [#667](https://github.com/tiangolo/sqlmodel/pull/667) by [@tiangolo](https://github.com/tiangolo). * 🎨 Run pre-commit on all files and autoformat. PR [#666](https://github.com/tiangolo/sqlmodel/pull/666) by [@tiangolo](https://github.com/tiangolo). * 👷 Move to Ruff and add pre-commit. PR [#661](https://github.com/tiangolo/sqlmodel/pull/661) by [@tiangolo](https://github.com/tiangolo). -* ✨ Raise a more clear error when a type is not valid. PR [#425](https://github.com/tiangolo/sqlmodel/pull/425) by [@ddanier](https://github.com/ddanier). -* 📝 Update outdated link in `docs/db-to-code.md`. PR [#649](https://github.com/tiangolo/sqlmodel/pull/649) by [@MatveyF](https://github.com/MatveyF). -* ✏️ Fix typos found with codespell. PR [#520](https://github.com/tiangolo/sqlmodel/pull/520) by [@kianmeng](https://github.com/kianmeng). -* 📝 Fix typos (duplication) in main page. PR [#631](https://github.com/tiangolo/sqlmodel/pull/631) by [@Mr-DRP](https://github.com/Mr-DRP). -* 📝 Update release notes, add second author to PR. PR [#429](https://github.com/tiangolo/sqlmodel/pull/429) by [@br-follow](https://github.com/br-follow). -* 📝 Update instructions about how to make a foreign key required in `docs/tutorial/relationship-attributes/define-relationships-attributes.md`. PR [#474](https://github.com/tiangolo/sqlmodel/pull/474) by [@jalvaradosegura](https://github.com/jalvaradosegura). -* 🐛 Fix SQLAlchemy version 1.4.36 breaks SQLModel relationships (#315). PR [#461](https://github.com/tiangolo/sqlmodel/pull/461) by [@byrman](https://github.com/byrman). * 🛠️ Add `CITATION.cff` file for academic citations. PR [#13](https://github.com/tiangolo/sqlmodel/pull/13) by [@sugatoray](https://github.com/sugatoray). * 👷 Update docs deployments to Cloudflare. PR [#630](https://github.com/tiangolo/sqlmodel/pull/630) by [@tiangolo](https://github.com/tiangolo). * 👷‍♂️ Upgrade CI for docs. PR [#628](https://github.com/tiangolo/sqlmodel/pull/628) by [@tiangolo](https://github.com/tiangolo). -* 🗑️ Deprecate Python 3.6 and upgrade Poetry and Poetry Version Plugin. PR [#627](https://github.com/tiangolo/sqlmodel/pull/627) by [@tiangolo](https://github.com/tiangolo). * 👷 Update CI debug mode with Tmate. PR [#629](https://github.com/tiangolo/sqlmodel/pull/629) by [@tiangolo](https://github.com/tiangolo). * 👷 Update latest changes token. PR [#616](https://github.com/tiangolo/sqlmodel/pull/616) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade analytics. PR [#558](https://github.com/tiangolo/sqlmodel/pull/558) by [@tiangolo](https://github.com/tiangolo). -* 📝 Update help SQLModel docs. PR [#548](https://github.com/tiangolo/sqlmodel/pull/548) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update new issue chooser to point to GitHub Discussions. PR [#546](https://github.com/tiangolo/sqlmodel/pull/546) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add template for GitHub Discussion questions and update issues template. PR [#544](https://github.com/tiangolo/sqlmodel/pull/544) by [@tiangolo](https://github.com/tiangolo). * 👷 Refactor CI artifact upload/download for docs previews. PR [#514](https://github.com/tiangolo/sqlmodel/pull/514) by [@tiangolo](https://github.com/tiangolo). -* ✏️ Fix typo in internal function name `get_sqlachemy_type()`. PR [#496](https://github.com/tiangolo/sqlmodel/pull/496) by [@cmarqu](https://github.com/cmarqu). * ⬆ Bump actions/cache from 2 to 3. PR [#497](https://github.com/tiangolo/sqlmodel/pull/497) by [@dependabot[bot]](https://github.com/apps/dependabot). -* ✏️ Fix typo in docs. PR [#446](https://github.com/tiangolo/sqlmodel/pull/446) by [@davidbrochart](https://github.com/davidbrochart). * ⬆ Bump dawidd6/action-download-artifact from 2.24.0 to 2.24.2. PR [#493](https://github.com/tiangolo/sqlmodel/pull/493) by [@dependabot[bot]](https://github.com/apps/dependabot). -* ✏️ Fix typo in `docs/tutorial/create-db-and-table.md`. PR [#477](https://github.com/tiangolo/sqlmodel/pull/477) by [@FluffyDietEngine](https://github.com/FluffyDietEngine). -* ✏️ Fix small typos in docs. PR [#481](https://github.com/tiangolo/sqlmodel/pull/481) by [@micuffaro](https://github.com/micuffaro). * 🔧 Update Smokeshow coverage threshold. PR [#487](https://github.com/tiangolo/sqlmodel/pull/487) by [@tiangolo](https://github.com/tiangolo). * 👷 Move from Codecov to Smokeshow. PR [#486](https://github.com/tiangolo/sqlmodel/pull/486) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump actions/setup-python from 2 to 4. PR [#411](https://github.com/tiangolo/sqlmodel/pull/411) by [@dependabot[bot]](https://github.com/apps/dependabot). From d05c3ee495bb81d2915b38052ac5f1909da60e37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 24 Oct 2023 01:01:18 +0400 Subject: [PATCH 265/906] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.0.?= =?UTF-8?q?9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 2 ++ sqlmodel/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 9e95ad3cab..900804d8d1 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest Changes +## 0.0.9 + ### Breaking Changes * 🗑️ Deprecate Python 3.6 and upgrade Poetry and Poetry Version Plugin. PR [#627](https://github.com/tiangolo/sqlmodel/pull/627) by [@tiangolo](https://github.com/tiangolo). diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index 3aa6e0d2ac..b51084b3fc 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.0.8" +__version__ = "0.0.9" # Re-export from SQLAlchemy from sqlalchemy.engine import create_mock_engine as create_mock_engine From 99f8ce3894564444f33770577a934749e479034e Mon Sep 17 00:00:00 2001 From: Daniil Fajnberg <60156134+daniil-berg@users.noreply.github.com> Date: Thu, 26 Oct 2023 12:18:05 +0200 Subject: [PATCH 266/906] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20all?= =?UTF-8?q?=20`Field`=20parameters=20from=20Pydantic=20`1.9.0`=20and=20abo?= =?UTF-8?q?ve,=20make=20Pydantic=20`1.9.0`=20the=20minimum=20required=20ve?= =?UTF-8?q?rsion=20(#440)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- pyproject.toml | 2 +- sqlmodel/main.py | 16 ++++++++- tests/test_pydantic/__init__.py | 0 tests/test_pydantic/test_field.py | 57 +++++++++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 tests/test_pydantic/__init__.py create mode 100644 tests/test_pydantic/test_field.py diff --git a/pyproject.toml b/pyproject.toml index c7956daaa9..181064e4ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ classifiers = [ [tool.poetry.dependencies] python = "^3.7" SQLAlchemy = ">=1.4.36,<2.0.0" -pydantic = "^1.8.2" +pydantic = "^1.9.0" sqlalchemy2-stubs = {version = "*", allow-prereleases = true} [tool.poetry.group.dev.dependencies] diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 07e600e4d4..3015aa9fbd 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -145,12 +145,17 @@ def Field( lt: Optional[float] = None, le: Optional[float] = None, multiple_of: Optional[float] = None, + max_digits: Optional[int] = None, + decimal_places: Optional[int] = None, min_items: Optional[int] = None, max_items: Optional[int] = None, + unique_items: Optional[bool] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, allow_mutation: bool = True, regex: Optional[str] = None, + discriminator: Optional[str] = None, + repr: bool = True, primary_key: bool = False, foreign_key: Optional[Any] = None, unique: bool = False, @@ -176,12 +181,17 @@ def Field( lt=lt, le=le, multiple_of=multiple_of, + max_digits=max_digits, + decimal_places=decimal_places, min_items=min_items, max_items=max_items, + unique_items=unique_items, min_length=min_length, max_length=max_length, allow_mutation=allow_mutation, regex=regex, + discriminator=discriminator, + repr=repr, primary_key=primary_key, foreign_key=foreign_key, unique=unique, @@ -587,7 +597,11 @@ def parse_obj( def __repr_args__(self) -> Sequence[Tuple[Optional[str], Any]]: # Don't show SQLAlchemy private attributes - return [(k, v) for k, v in self.__dict__.items() if not k.startswith("_sa_")] + return [ + (k, v) + for k, v in super().__repr_args__() + if not (isinstance(k, str) and k.startswith("_sa_")) + ] # From Pydantic, override to enforce validation with dict @classmethod diff --git a/tests/test_pydantic/__init__.py b/tests/test_pydantic/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_pydantic/test_field.py b/tests/test_pydantic/test_field.py new file mode 100644 index 0000000000..9d7bc77625 --- /dev/null +++ b/tests/test_pydantic/test_field.py @@ -0,0 +1,57 @@ +from decimal import Decimal +from typing import Optional, Union + +import pytest +from pydantic import ValidationError +from sqlmodel import Field, SQLModel +from typing_extensions import Literal + + +def test_decimal(): + class Model(SQLModel): + dec: Decimal = Field(max_digits=4, decimal_places=2) + + Model(dec=Decimal("3.14")) + Model(dec=Decimal("69.42")) + + with pytest.raises(ValidationError): + Model(dec=Decimal("3.142")) + with pytest.raises(ValidationError): + Model(dec=Decimal("0.069")) + with pytest.raises(ValidationError): + Model(dec=Decimal("420")) + + +def test_discriminator(): + # Example adapted from + # [Pydantic docs](https://pydantic-docs.helpmanual.io/usage/types/#discriminated-unions-aka-tagged-unions): + + class Cat(SQLModel): + pet_type: Literal["cat"] + meows: int + + class Dog(SQLModel): + pet_type: Literal["dog"] + barks: float + + class Lizard(SQLModel): + pet_type: Literal["reptile", "lizard"] + scales: bool + + class Model(SQLModel): + pet: Union[Cat, Dog, Lizard] = Field(..., discriminator="pet_type") + n: int + + Model(pet={"pet_type": "dog", "barks": 3.14}, n=1) # type: ignore[arg-type] + + with pytest.raises(ValidationError): + Model(pet={"pet_type": "dog"}, n=1) # type: ignore[arg-type] + + +def test_repr(): + class Model(SQLModel): + id: Optional[int] = Field(primary_key=True) + foo: str = Field(repr=False) + + instance = Model(id=123, foo="bar") + assert "foo=" not in repr(instance) From 8d1423253812af999aed55ce56d2918211039b08 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 26 Oct 2023 10:18:41 +0000 Subject: [PATCH 267/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 900804d8d1..4956a12474 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Add support for all `Field` parameters from Pydantic `1.9.0` and above, make Pydantic `1.9.0` the minimum required version. PR [#440](https://github.com/tiangolo/sqlmodel/pull/440) by [@daniil-berg](https://github.com/daniil-berg). ## 0.0.9 ### Breaking Changes From 7fdfee10a5275b6f076d18d70e584da6b632a313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 26 Oct 2023 18:32:26 +0400 Subject: [PATCH 268/906] =?UTF-8?q?=F0=9F=94=A7=20Adopt=20Ruff=20for=20for?= =?UTF-8?q?matting=20(#679)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 16 +++------------- pyproject.toml | 9 ++++++++- scripts/format.sh | 2 +- scripts/lint.sh | 2 +- tests/conftest.py | 3 +-- 5 files changed, 14 insertions(+), 18 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 022ef24a0f..61aaf71411 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,23 +13,13 @@ repos: - --unsafe - id: end-of-file-fixer - id: trailing-whitespace -- repo: https://github.com/asottile/pyupgrade - rev: v3.15.0 - hooks: - - id: pyupgrade - args: - - --py3-plus - - --keep-runtime-typing -- repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.1 +- repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.1.2 hooks: - id: ruff args: - --fix -- repo: https://github.com/psf/black - rev: 23.10.0 - hooks: - - id: black + - id: ruff-format ci: autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate diff --git a/pyproject.toml b/pyproject.toml index 181064e4ea..20188513c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ sqlalchemy2-stubs = {version = "*", allow-prereleases = true} [tool.poetry.group.dev.dependencies] pytest = "^7.0.1" mypy = "0.971" +# Needed by the code generator using templates black = "^22.10.0" mkdocs-material = "9.1.21" pillow = "^9.3.0" @@ -46,7 +47,7 @@ mdx-include = "^1.4.1" coverage = {extras = ["toml"], version = "^6.2"} fastapi = "^0.68.1" requests = "^2.26.0" -ruff = "^0.1.1" +ruff = "^0.1.2" [build-system] requires = ["poetry-core"] @@ -87,11 +88,13 @@ select = [ "I", # isort "C", # flake8-comprehensions "B", # flake8-bugbear + "UP", # pyupgrade ] ignore = [ "E501", # line too long, handled by black "B008", # do not perform function calls in argument defaults "C901", # too complex + "W191", # indentation contains tabs ] [tool.ruff.per-file-ignores] @@ -99,3 +102,7 @@ ignore = [ [tool.ruff.isort] known-third-party = ["sqlmodel", "sqlalchemy", "pydantic", "fastapi"] + +[tool.ruff.pyupgrade] +# Preserve types, even if a file imports `from __future__ import annotations`. +keep-runtime-typing = true diff --git a/scripts/format.sh b/scripts/format.sh index b6aebd10d4..70c12e579d 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -2,4 +2,4 @@ set -x ruff sqlmodel tests docs_src scripts --fix -black sqlmodel tests docs_src scripts +ruff format sqlmodel tests docs_src scripts diff --git a/scripts/lint.sh b/scripts/lint.sh index b328e3d9ac..f66882239f 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -5,4 +5,4 @@ set -x mypy sqlmodel ruff sqlmodel tests docs_src scripts -black sqlmodel tests docs_src --check +ruff format sqlmodel tests docs_src --check diff --git a/tests/conftest.py b/tests/conftest.py index cd66420c88..2b8e5fc29e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -42,8 +42,7 @@ def coverage_run(*, module: str, cwd: Union[str, Path]) -> subprocess.CompletedP module, ], cwd=str(cwd), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, + capture_output=True, encoding="utf-8", ) return result From 13cc722110b9d3b37cd00a9a0480fdd4bccb289a Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 26 Oct 2023 14:32:59 +0000 Subject: [PATCH 269/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 4956a12474..11ad6fc232 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Adopt Ruff for formatting. PR [#679](https://github.com/tiangolo/sqlmodel/pull/679) by [@tiangolo](https://github.com/tiangolo). * ✨ Add support for all `Field` parameters from Pydantic `1.9.0` and above, make Pydantic `1.9.0` the minimum required version. PR [#440](https://github.com/tiangolo/sqlmodel/pull/440) by [@daniil-berg](https://github.com/daniil-berg). ## 0.0.9 From e4e1385eedc700ad8c4e079841a85c32a29f1cff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 26 Oct 2023 18:34:49 +0400 Subject: [PATCH 270/906] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.0.?= =?UTF-8?q?10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 10 +++++++++- sqlmodel/__init__.py | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 11ad6fc232..61cd9f66ad 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,8 +2,16 @@ ## Latest Changes -* 🔧 Adopt Ruff for formatting. PR [#679](https://github.com/tiangolo/sqlmodel/pull/679) by [@tiangolo](https://github.com/tiangolo). +## 0.0.10 + +### Features + * ✨ Add support for all `Field` parameters from Pydantic `1.9.0` and above, make Pydantic `1.9.0` the minimum required version. PR [#440](https://github.com/tiangolo/sqlmodel/pull/440) by [@daniil-berg](https://github.com/daniil-berg). + +### Internal + +* 🔧 Adopt Ruff for formatting. PR [#679](https://github.com/tiangolo/sqlmodel/pull/679) by [@tiangolo](https://github.com/tiangolo). + ## 0.0.9 ### Breaking Changes diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index b51084b3fc..6b65860d30 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.0.9" +__version__ = "0.0.10" # Re-export from SQLAlchemy from sqlalchemy.engine import create_mock_engine as create_mock_engine From 717594ef13b64d0d4fd1ce7c6305c945f68460d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 28 Oct 2023 17:55:23 +0400 Subject: [PATCH 271/906] =?UTF-8?q?=E2=9C=A8=20Do=20not=20allow=20invalid?= =?UTF-8?q?=20combinations=20of=20field=20parameters=20for=20columns=20and?= =?UTF-8?q?=20relationships,=20`sa=5Fcolumn`=20excludes=20`sa=5Fcolumn=5Fa?= =?UTF-8?q?rgs`,=20`primary=5Fkey`,=20`nullable`,=20etc.=20(#681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️ Make sa_column exclusive, do not allow incompatible arguments, sa_column_args, primary_key, etc * ✅ Add tests for new errors when incorrectly using sa_column * ✅ Add tests for sa_column_args and sa_column_kwargs * ♻️ Do not allow sa_relationship with sa_relationship_args or sa_relationship_kwargs * ✅ Add tests for relationship errors * ✅ Fix test for sa_column_args --- sqlmodel/main.py | 151 ++++++++++++++++++++++++++-- tests/test_field_sa_args_kwargs.py | 39 +++++++ tests/test_field_sa_column.py | 99 ++++++++++++++++++ tests/test_field_sa_relationship.py | 53 ++++++++++ 4 files changed, 332 insertions(+), 10 deletions(-) create mode 100644 tests/test_field_sa_args_kwargs.py create mode 100644 tests/test_field_sa_column.py create mode 100644 tests/test_field_sa_relationship.py diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 3015aa9fbd..f48e388e13 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -22,6 +22,7 @@ TypeVar, Union, cast, + overload, ) from pydantic import BaseConfig, BaseModel @@ -87,6 +88,28 @@ def __init__(self, default: Any = Undefined, **kwargs: Any) -> None: "Passing sa_column_kwargs is not supported when " "also passing a sa_column" ) + if primary_key is not Undefined: + raise RuntimeError( + "Passing primary_key is not supported when " + "also passing a sa_column" + ) + if nullable is not Undefined: + raise RuntimeError( + "Passing nullable is not supported when " "also passing a sa_column" + ) + if foreign_key is not Undefined: + raise RuntimeError( + "Passing foreign_key is not supported when " + "also passing a sa_column" + ) + if unique is not Undefined: + raise RuntimeError( + "Passing unique is not supported when " "also passing a sa_column" + ) + if index is not Undefined: + raise RuntimeError( + "Passing index is not supported when " "also passing a sa_column" + ) super().__init__(default=default, **kwargs) self.primary_key = primary_key self.nullable = nullable @@ -126,6 +149,86 @@ def __init__( self.sa_relationship_kwargs = sa_relationship_kwargs +@overload +def Field( + default: Any = Undefined, + *, + default_factory: Optional[NoArgAnyCallable] = None, + alias: Optional[str] = None, + title: Optional[str] = None, + description: Optional[str] = None, + exclude: Union[ + AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any + ] = None, + include: Union[ + AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any + ] = None, + const: Optional[bool] = None, + gt: Optional[float] = None, + ge: Optional[float] = None, + lt: Optional[float] = None, + le: Optional[float] = None, + multiple_of: Optional[float] = None, + max_digits: Optional[int] = None, + decimal_places: Optional[int] = None, + min_items: Optional[int] = None, + max_items: Optional[int] = None, + unique_items: Optional[bool] = None, + min_length: Optional[int] = None, + max_length: Optional[int] = None, + allow_mutation: bool = True, + regex: Optional[str] = None, + discriminator: Optional[str] = None, + repr: bool = True, + primary_key: Union[bool, UndefinedType] = Undefined, + foreign_key: Any = Undefined, + unique: Union[bool, UndefinedType] = Undefined, + nullable: Union[bool, UndefinedType] = Undefined, + index: Union[bool, UndefinedType] = Undefined, + sa_column_args: Union[Sequence[Any], UndefinedType] = Undefined, + sa_column_kwargs: Union[Mapping[str, Any], UndefinedType] = Undefined, + schema_extra: Optional[Dict[str, Any]] = None, +) -> Any: + ... + + +@overload +def Field( + default: Any = Undefined, + *, + default_factory: Optional[NoArgAnyCallable] = None, + alias: Optional[str] = None, + title: Optional[str] = None, + description: Optional[str] = None, + exclude: Union[ + AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any + ] = None, + include: Union[ + AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any + ] = None, + const: Optional[bool] = None, + gt: Optional[float] = None, + ge: Optional[float] = None, + lt: Optional[float] = None, + le: Optional[float] = None, + multiple_of: Optional[float] = None, + max_digits: Optional[int] = None, + decimal_places: Optional[int] = None, + min_items: Optional[int] = None, + max_items: Optional[int] = None, + unique_items: Optional[bool] = None, + min_length: Optional[int] = None, + max_length: Optional[int] = None, + allow_mutation: bool = True, + regex: Optional[str] = None, + discriminator: Optional[str] = None, + repr: bool = True, + sa_column: Union[Column, UndefinedType] = Undefined, # type: ignore + schema_extra: Optional[Dict[str, Any]] = None, +) -> Any: + ... + + def Field( default: Any = Undefined, *, @@ -156,9 +259,9 @@ def Field( regex: Optional[str] = None, discriminator: Optional[str] = None, repr: bool = True, - primary_key: bool = False, - foreign_key: Optional[Any] = None, - unique: bool = False, + primary_key: Union[bool, UndefinedType] = Undefined, + foreign_key: Any = Undefined, + unique: Union[bool, UndefinedType] = Undefined, nullable: Union[bool, UndefinedType] = Undefined, index: Union[bool, UndefinedType] = Undefined, sa_column: Union[Column, UndefinedType] = Undefined, # type: ignore @@ -206,6 +309,27 @@ def Field( return field_info +@overload +def Relationship( + *, + back_populates: Optional[str] = None, + link_model: Optional[Any] = None, + sa_relationship_args: Optional[Sequence[Any]] = None, + sa_relationship_kwargs: Optional[Mapping[str, Any]] = None, +) -> Any: + ... + + +@overload +def Relationship( + *, + back_populates: Optional[str] = None, + link_model: Optional[Any] = None, + sa_relationship: Optional[RelationshipProperty] = None, # type: ignore +) -> Any: + ... + + def Relationship( *, back_populates: Optional[str] = None, @@ -440,21 +564,28 @@ def get_column_from_field(field: ModelField) -> Column: # type: ignore if isinstance(sa_column, Column): return sa_column sa_type = get_sqlalchemy_type(field) - primary_key = getattr(field.field_info, "primary_key", False) + primary_key = getattr(field.field_info, "primary_key", Undefined) + if primary_key is Undefined: + primary_key = False index = getattr(field.field_info, "index", Undefined) if index is Undefined: index = False nullable = not primary_key and _is_field_noneable(field) # Override derived nullability if the nullable property is set explicitly # on the field - if hasattr(field.field_info, "nullable"): - field_nullable = getattr(field.field_info, "nullable") # noqa: B009 - if field_nullable != Undefined: - nullable = field_nullable + field_nullable = getattr(field.field_info, "nullable", Undefined) # noqa: B009 + if field_nullable != Undefined: + assert not isinstance(field_nullable, UndefinedType) + nullable = field_nullable args = [] - foreign_key = getattr(field.field_info, "foreign_key", None) - unique = getattr(field.field_info, "unique", False) + foreign_key = getattr(field.field_info, "foreign_key", Undefined) + if foreign_key is Undefined: + foreign_key = None + unique = getattr(field.field_info, "unique", Undefined) + if unique is Undefined: + unique = False if foreign_key: + assert isinstance(foreign_key, str) args.append(ForeignKey(foreign_key)) kwargs = { "primary_key": primary_key, diff --git a/tests/test_field_sa_args_kwargs.py b/tests/test_field_sa_args_kwargs.py new file mode 100644 index 0000000000..94a1a13483 --- /dev/null +++ b/tests/test_field_sa_args_kwargs.py @@ -0,0 +1,39 @@ +from typing import Optional + +from sqlalchemy import ForeignKey +from sqlmodel import Field, SQLModel, create_engine + + +def test_sa_column_args(clear_sqlmodel, caplog) -> None: + class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str + + class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + team_id: Optional[int] = Field( + default=None, + sa_column_args=[ForeignKey("team.id")], + ) + + engine = create_engine("sqlite://", echo=True) + SQLModel.metadata.create_all(engine) + create_table_log = [ + message for message in caplog.messages if "CREATE TABLE hero" in message + ][0] + assert "FOREIGN KEY(team_id) REFERENCES team (id)" in create_table_log + + +def test_sa_column_kargs(clear_sqlmodel, caplog) -> None: + class Item(SQLModel, table=True): + id: Optional[int] = Field( + default=None, + sa_column_kwargs={"primary_key": True}, + ) + + engine = create_engine("sqlite://", echo=True) + SQLModel.metadata.create_all(engine) + create_table_log = [ + message for message in caplog.messages if "CREATE TABLE item" in message + ][0] + assert "PRIMARY KEY (id)" in create_table_log diff --git a/tests/test_field_sa_column.py b/tests/test_field_sa_column.py new file mode 100644 index 0000000000..51cfdfa797 --- /dev/null +++ b/tests/test_field_sa_column.py @@ -0,0 +1,99 @@ +from typing import Optional + +import pytest +from sqlalchemy import Column, Integer, String +from sqlmodel import Field, SQLModel + + +def test_sa_column_takes_precedence() -> None: + class Item(SQLModel, table=True): + id: Optional[int] = Field( + default=None, + sa_column=Column(String, primary_key=True, nullable=False), + ) + + # It would have been nullable with no sa_column + assert Item.id.nullable is False # type: ignore + assert isinstance(Item.id.type, String) # type: ignore + + +def test_sa_column_no_sa_args() -> None: + with pytest.raises(RuntimeError): + + class Item(SQLModel, table=True): + id: Optional[int] = Field( + default=None, + sa_column_args=[Integer], + sa_column=Column(Integer, primary_key=True), + ) + + +def test_sa_column_no_sa_kargs() -> None: + with pytest.raises(RuntimeError): + + class Item(SQLModel, table=True): + id: Optional[int] = Field( + default=None, + sa_column_kwargs={"primary_key": True}, + sa_column=Column(Integer, primary_key=True), + ) + + +def test_sa_column_no_primary_key() -> None: + with pytest.raises(RuntimeError): + + class Item(SQLModel, table=True): + id: Optional[int] = Field( + default=None, + primary_key=True, + sa_column=Column(Integer, primary_key=True), + ) + + +def test_sa_column_no_nullable() -> None: + with pytest.raises(RuntimeError): + + class Item(SQLModel, table=True): + id: Optional[int] = Field( + default=None, + nullable=True, + sa_column=Column(Integer, primary_key=True), + ) + + +def test_sa_column_no_foreign_key() -> None: + with pytest.raises(RuntimeError): + + class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str + + class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + team_id: Optional[int] = Field( + default=None, + foreign_key="team.id", + sa_column=Column(Integer, primary_key=True), + ) + + +def test_sa_column_no_unique() -> None: + with pytest.raises(RuntimeError): + + class Item(SQLModel, table=True): + id: Optional[int] = Field( + default=None, + unique=True, + sa_column=Column(Integer, primary_key=True), + ) + + +def test_sa_column_no_index() -> None: + with pytest.raises(RuntimeError): + + class Item(SQLModel, table=True): + id: Optional[int] = Field( + default=None, + index=True, + sa_column=Column(Integer, primary_key=True), + ) diff --git a/tests/test_field_sa_relationship.py b/tests/test_field_sa_relationship.py new file mode 100644 index 0000000000..7606fd86d8 --- /dev/null +++ b/tests/test_field_sa_relationship.py @@ -0,0 +1,53 @@ +from typing import List, Optional + +import pytest +from sqlalchemy.orm import relationship +from sqlmodel import Field, Relationship, SQLModel + + +def test_sa_relationship_no_args() -> None: + with pytest.raises(RuntimeError): + + class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: List["Hero"] = Relationship( + back_populates="team", + sa_relationship_args=["Hero"], + sa_relationship=relationship("Hero", back_populates="team"), + ) + + class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + team_id: Optional[int] = Field(default=None, foreign_key="team.id") + team: Optional[Team] = Relationship(back_populates="heroes") + + +def test_sa_relationship_no_kwargs() -> None: + with pytest.raises(RuntimeError): + + class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: List["Hero"] = Relationship( + back_populates="team", + sa_relationship_kwargs={"lazy": "selectin"}, + sa_relationship=relationship("Hero", back_populates="team"), + ) + + class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + team_id: Optional[int] = Field(default=None, foreign_key="team.id") + team: Optional[Team] = Relationship(back_populates="heroes") From 6457775a0f6994cdaf48ecaf286bb16d4bd44840 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 28 Oct 2023 13:55:56 +0000 Subject: [PATCH 272/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 61cd9f66ad..9e5a876969 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Do not allow invalid combinations of field parameters for columns and relationships, `sa_column` excludes `sa_column_args`, `primary_key`, `nullable`, etc.. PR [#681](https://github.com/tiangolo/sqlmodel/pull/681) by [@tiangolo](https://github.com/tiangolo). ## 0.0.10 ### Features From 963298066402813273a2982e450f4eaf2f86ab79 Mon Sep 17 00:00:00 2001 From: Matthieu LAURENT Date: Sun, 29 Oct 2023 08:24:32 +0100 Subject: [PATCH 273/906] =?UTF-8?q?=F0=9F=8E=A8=20Update=20inline=20source?= =?UTF-8?q?=20examples,=20hide=20`#`=20in=20annotations=20(from=20MkDocs?= =?UTF-8?q?=20Material)=20(#677)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- .../automatic_id_none_refresh/tutorial002.py | 100 +++++++++--------- .../create_db_and_table/tutorial003.py | 28 ++--- docs_src/tutorial/delete/tutorial002.py | 26 ++--- .../app_testing/tutorial001/test_main_001.py | 18 ++-- .../app_testing/tutorial001/test_main_002.py | 10 +- .../app_testing/tutorial001/test_main_003.py | 14 +-- .../app_testing/tutorial001/test_main_004.py | 6 +- .../app_testing/tutorial001/test_main_005.py | 12 +-- .../app_testing/tutorial001/test_main_006.py | 16 +-- docs_src/tutorial/insert/tutorial003.py | 22 ++-- docs_src/tutorial/select/tutorial002.py | 26 ++--- docs_src/tutorial/update/tutorial002.py | 20 ++-- docs_src/tutorial/update/tutorial004.py | 50 ++++----- 13 files changed, 174 insertions(+), 174 deletions(-) diff --git a/docs_src/tutorial/automatic_id_none_refresh/tutorial002.py b/docs_src/tutorial/automatic_id_none_refresh/tutorial002.py index 1c7cd53e2f..c597506a62 100644 --- a/docs_src/tutorial/automatic_id_none_refresh/tutorial002.py +++ b/docs_src/tutorial/automatic_id_none_refresh/tutorial002.py @@ -21,56 +21,56 @@ def create_db_and_tables(): def create_heroes(): - hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") # (1) - hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") # (2) - hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) # (3) - - print("Before interacting with the database") # (4) - print("Hero 1:", hero_1) # (5) - print("Hero 2:", hero_2) # (6) - print("Hero 3:", hero_3) # (7) - - with Session(engine) as session: # (8) - session.add(hero_1) # (9) - session.add(hero_2) # (10) - session.add(hero_3) # (11) - - print("After adding to the session") # (12) - print("Hero 1:", hero_1) # (13) - print("Hero 2:", hero_2) # (14) - print("Hero 3:", hero_3) # (15) - - session.commit() # (16) - - print("After committing the session") # (17) - print("Hero 1:", hero_1) # (18) - print("Hero 2:", hero_2) # (19) - print("Hero 3:", hero_3) # (20) - - print("After committing the session, show IDs") # (21) - print("Hero 1 ID:", hero_1.id) # (22) - print("Hero 2 ID:", hero_2.id) # (23) - print("Hero 3 ID:", hero_3.id) # (24) - - print("After committing the session, show names") # (25) - print("Hero 1 name:", hero_1.name) # (26) - print("Hero 2 name:", hero_2.name) # (27) - print("Hero 3 name:", hero_3.name) # (28) - - session.refresh(hero_1) # (29) - session.refresh(hero_2) # (30) - session.refresh(hero_3) # (31) - - print("After refreshing the heroes") # (32) - print("Hero 1:", hero_1) # (33) - print("Hero 2:", hero_2) # (34) - print("Hero 3:", hero_3) # (35) - # (36) - - print("After the session closes") # (37) - print("Hero 1:", hero_1) # (38) - print("Hero 2:", hero_2) # (39) - print("Hero 3:", hero_3) # (40) + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") # (1)! + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") # (2)! + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) # (3)! + + print("Before interacting with the database") # (4)! + print("Hero 1:", hero_1) # (5)! + print("Hero 2:", hero_2) # (6)! + print("Hero 3:", hero_3) # (7)! + + with Session(engine) as session: # (8)! + session.add(hero_1) # (9)! + session.add(hero_2) # (10)! + session.add(hero_3) # (11)! + + print("After adding to the session") # (12)! + print("Hero 1:", hero_1) # (13)! + print("Hero 2:", hero_2) # (14)! + print("Hero 3:", hero_3) # (15)! + + session.commit() # (16)! + + print("After committing the session") # (17)! + print("Hero 1:", hero_1) # (18)! + print("Hero 2:", hero_2) # (19)! + print("Hero 3:", hero_3) # (20)! + + print("After committing the session, show IDs") # (21)! + print("Hero 1 ID:", hero_1.id) # (22)! + print("Hero 2 ID:", hero_2.id) # (23)! + print("Hero 3 ID:", hero_3.id) # (24)! + + print("After committing the session, show names") # (25)! + print("Hero 1 name:", hero_1.name) # (26)! + print("Hero 2 name:", hero_2.name) # (27)! + print("Hero 3 name:", hero_3.name) # (28)! + + session.refresh(hero_1) # (29)! + session.refresh(hero_2) # (30)! + session.refresh(hero_3) # (31)! + + print("After refreshing the heroes") # (32)! + print("Hero 1:", hero_1) # (33)! + print("Hero 2:", hero_2) # (34)! + print("Hero 3:", hero_3) # (35)! + # (36)! + + print("After the session closes") # (37)! + print("Hero 1:", hero_1) # (38)! + print("Hero 2:", hero_2) # (39)! + print("Hero 3:", hero_3) # (40)! def main(): diff --git a/docs_src/tutorial/create_db_and_table/tutorial003.py b/docs_src/tutorial/create_db_and_table/tutorial003.py index 86508709c1..9406300400 100644 --- a/docs_src/tutorial/create_db_and_table/tutorial003.py +++ b/docs_src/tutorial/create_db_and_table/tutorial003.py @@ -1,24 +1,24 @@ -from typing import Optional # (1) +from typing import Optional # (1)! -from sqlmodel import Field, SQLModel, create_engine # (2) +from sqlmodel import Field, SQLModel, create_engine # (2)! -class Hero(SQLModel, table=True): # (3) - id: Optional[int] = Field(default=None, primary_key=True) # (4) - name: str # (5) - secret_name: str # (6) - age: Optional[int] = None # (7) +class Hero(SQLModel, table=True): # (3)! + id: Optional[int] = Field(default=None, primary_key=True) # (4)! + name: str # (5)! + secret_name: str # (6)! + age: Optional[int] = None # (7)! -sqlite_file_name = "database.db" # (8) -sqlite_url = f"sqlite:///{sqlite_file_name}" # (9) +sqlite_file_name = "database.db" # (8)! +sqlite_url = f"sqlite:///{sqlite_file_name}" # (9)! -engine = create_engine(sqlite_url, echo=True) # (10) +engine = create_engine(sqlite_url, echo=True) # (10)! -def create_db_and_tables(): # (11) - SQLModel.metadata.create_all(engine) # (12) +def create_db_and_tables(): # (11)! + SQLModel.metadata.create_all(engine) # (12)! -if __name__ == "__main__": # (13) - create_db_and_tables() # (14) +if __name__ == "__main__": # (13)! + create_db_and_tables() # (14)! diff --git a/docs_src/tutorial/delete/tutorial002.py b/docs_src/tutorial/delete/tutorial002.py index 202d63b6d3..4d8c368d3c 100644 --- a/docs_src/tutorial/delete/tutorial002.py +++ b/docs_src/tutorial/delete/tutorial002.py @@ -71,23 +71,23 @@ def update_heroes(): def delete_heroes(): with Session(engine) as session: - statement = select(Hero).where(Hero.name == "Spider-Youngster") # (1) - results = session.exec(statement) # (2) - hero = results.one() # (3) - print("Hero: ", hero) # (4) + statement = select(Hero).where(Hero.name == "Spider-Youngster") # (1)! + results = session.exec(statement) # (2)! + hero = results.one() # (3)! + print("Hero: ", hero) # (4)! - session.delete(hero) # (5) - session.commit() # (6) + session.delete(hero) # (5)! + session.commit() # (6)! - print("Deleted hero:", hero) # (7) + print("Deleted hero:", hero) # (7)! - statement = select(Hero).where(Hero.name == "Spider-Youngster") # (8) - results = session.exec(statement) # (9) - hero = results.first() # (10) + statement = select(Hero).where(Hero.name == "Spider-Youngster") # (8)! + results = session.exec(statement) # (9)! + hero = results.first() # (10)! - if hero is None: # (11) - print("There's no hero named Spider-Youngster") # (12) - # (13) + if hero is None: # (11)! + print("There's no hero named Spider-Youngster") # (12)! + # (13)! def main(): diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_001.py b/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_001.py index 38ab1f1885..3ae40773f9 100644 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_001.py +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_001.py @@ -1,7 +1,7 @@ from fastapi.testclient import TestClient from sqlmodel import Session, SQLModel, create_engine -from .main import app, get_session # (1) +from .main import app, get_session # (1)! def test_create_hero(): @@ -17,16 +17,16 @@ def get_session_override(): app.dependency_overrides[get_session] = get_session_override - client = TestClient(app) # (2) + client = TestClient(app) # (2)! - response = client.post( # (3) + response = client.post( # (3)! "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} ) app.dependency_overrides.clear() - data = response.json() # (4) + data = response.json() # (4)! - assert response.status_code == 200 # (5) - assert data["name"] == "Deadpond" # (6) - assert data["secret_name"] == "Dive Wilson" # (7) - assert data["age"] is None # (8) - assert data["id"] is not None # (9) + assert response.status_code == 200 # (5)! + assert data["name"] == "Deadpond" # (6)! + assert data["secret_name"] == "Dive Wilson" # (7)! + assert data["age"] is None # (8)! + assert data["id"] is not None # (9)! diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_002.py b/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_002.py index 144360a820..727580b68f 100644 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_002.py +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_002.py @@ -1,7 +1,7 @@ from fastapi.testclient import TestClient from sqlmodel import Session, SQLModel, create_engine -from .main import app, get_session # (1) +from .main import app, get_session # (1)! def test_create_hero(): @@ -12,17 +12,17 @@ def test_create_hero(): with Session(engine) as session: - def get_session_override(): # (2) - return session # (3) + def get_session_override(): # (2)! + return session # (3)! - app.dependency_overrides[get_session] = get_session_override # (4) + app.dependency_overrides[get_session] = get_session_override # (4)! client = TestClient(app) response = client.post( "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} ) - app.dependency_overrides.clear() # (5) + app.dependency_overrides.clear() # (5)! data = response.json() assert response.status_code == 200 diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_003.py b/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_003.py index 5de06f8404..465c525108 100644 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_003.py +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_003.py @@ -1,21 +1,21 @@ from fastapi.testclient import TestClient from sqlmodel import Session, SQLModel, create_engine -from .main import app, get_session # (1) +from .main import app, get_session # (1)! def test_create_hero(): - engine = create_engine( # (2) + engine = create_engine( # (2)! "sqlite:///testing.db", connect_args={"check_same_thread": False} ) - SQLModel.metadata.create_all(engine) # (3) + SQLModel.metadata.create_all(engine) # (3)! - with Session(engine) as session: # (4) + with Session(engine) as session: # (4)! def get_session_override(): - return session # (5) + return session # (5)! - app.dependency_overrides[get_session] = get_session_override # (4) + app.dependency_overrides[get_session] = get_session_override # (4)! client = TestClient(app) @@ -30,4 +30,4 @@ def get_session_override(): assert data["secret_name"] == "Dive Wilson" assert data["age"] is None assert data["id"] is not None - # (6) + # (6)! diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_004.py b/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_004.py index f14fce8092..b770a9aa59 100644 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_004.py +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_004.py @@ -1,15 +1,15 @@ from fastapi.testclient import TestClient from sqlmodel import Session, SQLModel, create_engine -from sqlmodel.pool import StaticPool # (1) +from sqlmodel.pool import StaticPool # (1)! from .main import app, get_session def test_create_hero(): engine = create_engine( - "sqlite://", # (2) + "sqlite://", # (2)! connect_args={"check_same_thread": False}, - poolclass=StaticPool, # (3) + poolclass=StaticPool, # (3)! ) SQLModel.metadata.create_all(engine) diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_005.py b/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_005.py index b5200e6150..f653eef7ec 100644 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_005.py +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_005.py @@ -1,4 +1,4 @@ -import pytest # (1) +import pytest # (1)! from fastapi.testclient import TestClient from sqlmodel import Session, SQLModel, create_engine from sqlmodel.pool import StaticPool @@ -6,19 +6,19 @@ from .main import app, get_session -@pytest.fixture(name="session") # (2) -def session_fixture(): # (3) +@pytest.fixture(name="session") # (2)! +def session_fixture(): # (3)! engine = create_engine( "sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool ) SQLModel.metadata.create_all(engine) with Session(engine) as session: - yield session # (4) + yield session # (4)! -def test_create_hero(session: Session): # (5) +def test_create_hero(session: Session): # (5)! def get_session_override(): - return session # (6) + return session # (6)! app.dependency_overrides[get_session] = get_session_override diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_006.py b/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_006.py index 577d56ddaf..8dbfd45caf 100644 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_006.py +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_006.py @@ -16,19 +16,19 @@ def session_fixture(): yield session -@pytest.fixture(name="client") # (1) -def client_fixture(session: Session): # (2) - def get_session_override(): # (3) +@pytest.fixture(name="client") # (1)! +def client_fixture(session: Session): # (2)! + def get_session_override(): # (3)! return session - app.dependency_overrides[get_session] = get_session_override # (4) + app.dependency_overrides[get_session] = get_session_override # (4)! - client = TestClient(app) # (5) - yield client # (6) - app.dependency_overrides.clear() # (7) + client = TestClient(app) # (5)! + yield client # (6)! + app.dependency_overrides.clear() # (7)! -def test_create_hero(client: TestClient): # (8) +def test_create_hero(client: TestClient): # (8)! response = client.post( "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} ) diff --git a/docs_src/tutorial/insert/tutorial003.py b/docs_src/tutorial/insert/tutorial003.py index 8133f2901b..03f51d2359 100644 --- a/docs_src/tutorial/insert/tutorial003.py +++ b/docs_src/tutorial/insert/tutorial003.py @@ -20,24 +20,24 @@ def create_db_and_tables(): SQLModel.metadata.create_all(engine) -def create_heroes(): # (1) - hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") # (2) +def create_heroes(): # (1)! + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") # (2)! hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) - with Session(engine) as session: # (3) - session.add(hero_1) # (4) + with Session(engine) as session: # (3)! + session.add(hero_1) # (4)! session.add(hero_2) session.add(hero_3) - session.commit() # (5) - # (6) + session.commit() # (5)! + # (6)! -def main(): # (7) - create_db_and_tables() # (8) - create_heroes() # (9) +def main(): # (7)! + create_db_and_tables() # (8)! + create_heroes() # (9)! -if __name__ == "__main__": # (10) - main() # (11) +if __name__ == "__main__": # (10)! + main() # (11)! diff --git a/docs_src/tutorial/select/tutorial002.py b/docs_src/tutorial/select/tutorial002.py index 4f6d102827..2229f3fc30 100644 --- a/docs_src/tutorial/select/tutorial002.py +++ b/docs_src/tutorial/select/tutorial002.py @@ -1,9 +1,9 @@ from typing import Optional -from sqlmodel import Field, Session, SQLModel, create_engine, select # (1) +from sqlmodel import Field, Session, SQLModel, create_engine, select # (1)! -class Hero(SQLModel, table=True): # (2) +class Hero(SQLModel, table=True): # (2)! id: Optional[int] = Field(default=None, primary_key=True) name: str secret_name: str @@ -13,19 +13,19 @@ class Hero(SQLModel, table=True): # (2) sqlite_file_name = "database.db" sqlite_url = f"sqlite:///{sqlite_file_name}" -engine = create_engine(sqlite_url, echo=True) # (3) +engine = create_engine(sqlite_url, echo=True) # (3)! def create_db_and_tables(): - SQLModel.metadata.create_all(engine) # (4) + SQLModel.metadata.create_all(engine) # (4)! def create_heroes(): - hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") # (5) + 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) - with Session(engine) as session: # (6) + with Session(engine) as session: # (6)! session.add(hero_1) session.add(hero_2) session.add(hero_3) @@ -34,18 +34,18 @@ def create_heroes(): def select_heroes(): - with Session(engine) as session: # (7) - statement = select(Hero) # (8) - results = session.exec(statement) # (9) - for hero in results: # (10) - print(hero) # (11) - # (12) + with Session(engine) as session: # (7)! + statement = select(Hero) # (8)! + results = session.exec(statement) # (9)! + for hero in results: # (10)! + print(hero) # (11)! + # (12)! def main(): create_db_and_tables() create_heroes() - select_heroes() # (13) + select_heroes() # (13)! if __name__ == "__main__": diff --git a/docs_src/tutorial/update/tutorial002.py b/docs_src/tutorial/update/tutorial002.py index 4880f73f90..572198563c 100644 --- a/docs_src/tutorial/update/tutorial002.py +++ b/docs_src/tutorial/update/tutorial002.py @@ -43,16 +43,16 @@ def create_heroes(): def update_heroes(): with Session(engine) as session: - statement = select(Hero).where(Hero.name == "Spider-Boy") # (1) - results = session.exec(statement) # (2) - hero = results.one() # (3) - print("Hero:", hero) # (4) - - hero.age = 16 # (5) - session.add(hero) # (6) - session.commit() # (7) - session.refresh(hero) # (8) - print("Updated hero:", hero) # (9) + statement = select(Hero).where(Hero.name == "Spider-Boy") # (1)! + results = session.exec(statement) # (2)! + hero = results.one() # (3)! + print("Hero:", hero) # (4)! + + hero.age = 16 # (5)! + session.add(hero) # (6)! + session.commit() # (7)! + session.refresh(hero) # (8)! + print("Updated hero:", hero) # (9)! def main(): diff --git a/docs_src/tutorial/update/tutorial004.py b/docs_src/tutorial/update/tutorial004.py index 868c66c7d4..c74316e7b6 100644 --- a/docs_src/tutorial/update/tutorial004.py +++ b/docs_src/tutorial/update/tutorial004.py @@ -43,31 +43,31 @@ def create_heroes(): def update_heroes(): with Session(engine) as session: - statement = select(Hero).where(Hero.name == "Spider-Boy") # (1) - results = session.exec(statement) # (2) - hero_1 = results.one() # (3) - print("Hero 1:", hero_1) # (4) - - statement = select(Hero).where(Hero.name == "Captain North America") # (5) - results = session.exec(statement) # (6) - hero_2 = results.one() # (7) - print("Hero 2:", hero_2) # (8) - - hero_1.age = 16 # (9) - hero_1.name = "Spider-Youngster" # (10) - session.add(hero_1) # (11) - - hero_2.name = "Captain North America Except Canada" # (12) - hero_2.age = 110 # (13) - session.add(hero_2) # (14) - - session.commit() # (15) - session.refresh(hero_1) # (16) - session.refresh(hero_2) # (17) - - print("Updated hero 1:", hero_1) # (18) - print("Updated hero 2:", hero_2) # (19) - # (20) + statement = select(Hero).where(Hero.name == "Spider-Boy") # (1)! + results = session.exec(statement) # (2)! + hero_1 = results.one() # (3)! + print("Hero 1:", hero_1) # (4)! + + statement = select(Hero).where(Hero.name == "Captain North America") # (5)! + results = session.exec(statement) # (6)! + hero_2 = results.one() # (7)! + print("Hero 2:", hero_2) # (8)! + + hero_1.age = 16 # (9)! + hero_1.name = "Spider-Youngster" # (10)! + session.add(hero_1) # (11)! + + hero_2.name = "Captain North America Except Canada" # (12)! + hero_2.age = 110 # (13)! + session.add(hero_2) # (14)! + + session.commit() # (15)! + session.refresh(hero_1) # (16)! + session.refresh(hero_2) # (17)! + + print("Updated hero 1:", hero_1) # (18)! + print("Updated hero 2:", hero_2) # (19)! + # (20)! def main(): From c557cf6d18a4a173cd922e34831989f8caab1c3e Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 29 Oct 2023 07:25:17 +0000 Subject: [PATCH 274/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 9e5a876969..f01af6ca32 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🎨 Update inline source examples, hide `#` in annotations (from MkDocs Material). PR [#677](https://github.com/tiangolo/sqlmodel/pull/677) by [@Matthieu-LAURENT39](https://github.com/Matthieu-LAURENT39). * ✨ Do not allow invalid combinations of field parameters for columns and relationships, `sa_column` excludes `sa_column_args`, `primary_key`, `nullable`, etc.. PR [#681](https://github.com/tiangolo/sqlmodel/pull/681) by [@tiangolo](https://github.com/tiangolo). ## 0.0.10 From cbaf172c63889985e94031fb38ff00e80b7c90bf Mon Sep 17 00:00:00 2001 From: "Maruo.S" Date: Sun, 29 Oct 2023 17:10:39 +0900 Subject: [PATCH 275/906] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20passi?= =?UTF-8?q?ng=20a=20custom=20SQLAlchemy=20type=20to=20`Field()`=20with=20`?= =?UTF-8?q?sa=5Ftype`=20(#505)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- sqlmodel/main.py | 16 ++++++++++++++-- tests/test_field_sa_column.py | 11 +++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index f48e388e13..2b69dd2a75 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -74,6 +74,7 @@ def __init__(self, default: Any = Undefined, **kwargs: Any) -> None: foreign_key = kwargs.pop("foreign_key", Undefined) unique = kwargs.pop("unique", False) index = kwargs.pop("index", Undefined) + sa_type = kwargs.pop("sa_type", Undefined) sa_column = kwargs.pop("sa_column", Undefined) sa_column_args = kwargs.pop("sa_column_args", Undefined) sa_column_kwargs = kwargs.pop("sa_column_kwargs", Undefined) @@ -104,11 +105,15 @@ def __init__(self, default: Any = Undefined, **kwargs: Any) -> None: ) if unique is not Undefined: raise RuntimeError( - "Passing unique is not supported when " "also passing a sa_column" + "Passing unique is not supported when also passing a sa_column" ) if index is not Undefined: raise RuntimeError( - "Passing index is not supported when " "also passing a sa_column" + "Passing index is not supported when also passing a sa_column" + ) + if sa_type is not Undefined: + raise RuntimeError( + "Passing sa_type is not supported when also passing a sa_column" ) super().__init__(default=default, **kwargs) self.primary_key = primary_key @@ -116,6 +121,7 @@ def __init__(self, default: Any = Undefined, **kwargs: Any) -> None: self.foreign_key = foreign_key self.unique = unique self.index = index + self.sa_type = sa_type self.sa_column = sa_column self.sa_column_args = sa_column_args self.sa_column_kwargs = sa_column_kwargs @@ -185,6 +191,7 @@ def Field( unique: Union[bool, UndefinedType] = Undefined, nullable: Union[bool, UndefinedType] = Undefined, index: Union[bool, UndefinedType] = Undefined, + sa_type: Union[Type[Any], UndefinedType] = Undefined, sa_column_args: Union[Sequence[Any], UndefinedType] = Undefined, sa_column_kwargs: Union[Mapping[str, Any], UndefinedType] = Undefined, schema_extra: Optional[Dict[str, Any]] = None, @@ -264,6 +271,7 @@ def Field( unique: Union[bool, UndefinedType] = Undefined, nullable: Union[bool, UndefinedType] = Undefined, index: Union[bool, UndefinedType] = Undefined, + sa_type: Union[Type[Any], UndefinedType] = Undefined, sa_column: Union[Column, UndefinedType] = Undefined, # type: ignore sa_column_args: Union[Sequence[Any], UndefinedType] = Undefined, sa_column_kwargs: Union[Mapping[str, Any], UndefinedType] = Undefined, @@ -300,6 +308,7 @@ def Field( unique=unique, nullable=nullable, index=index, + sa_type=sa_type, sa_column=sa_column, sa_column_args=sa_column_args, sa_column_kwargs=sa_column_kwargs, @@ -515,6 +524,9 @@ def __init__( def get_sqlalchemy_type(field: ModelField) -> Any: + sa_type = getattr(field.field_info, "sa_type", Undefined) # noqa: B009 + if sa_type is not Undefined: + return sa_type if isinstance(field.type_, type) and field.shape == SHAPE_SINGLETON: # Check enums first as an enum can also be a str, needed by Pydantic/FastAPI if issubclass(field.type_, Enum): diff --git a/tests/test_field_sa_column.py b/tests/test_field_sa_column.py index 51cfdfa797..7384f1fabc 100644 --- a/tests/test_field_sa_column.py +++ b/tests/test_field_sa_column.py @@ -39,6 +39,17 @@ class Item(SQLModel, table=True): ) +def test_sa_column_no_type() -> None: + with pytest.raises(RuntimeError): + + class Item(SQLModel, table=True): + id: Optional[int] = Field( + default=None, + sa_type=Integer, + sa_column=Column(Integer, primary_key=True), + ) + + def test_sa_column_no_primary_key() -> None: with pytest.raises(RuntimeError): From 31ed654dd01fc3aab499722640e7d09073b1d2d8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 29 Oct 2023 08:11:21 +0000 Subject: [PATCH 276/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index f01af6ca32..acff20b4d3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Add support for passing a custom SQLAlchemy type to `Field()` with `sa_type`. PR [#505](https://github.com/tiangolo/sqlmodel/pull/505) by [@maru0123-2004](https://github.com/maru0123-2004). * 🎨 Update inline source examples, hide `#` in annotations (from MkDocs Material). PR [#677](https://github.com/tiangolo/sqlmodel/pull/677) by [@Matthieu-LAURENT39](https://github.com/Matthieu-LAURENT39). * ✨ Do not allow invalid combinations of field parameters for columns and relationships, `sa_column` excludes `sa_column_args`, `primary_key`, `nullable`, etc.. PR [#681](https://github.com/tiangolo/sqlmodel/pull/681) by [@tiangolo](https://github.com/tiangolo). ## 0.0.10 From 94f3765fcfee0cd0b2be057c3e9cc6b66b1049b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 29 Oct 2023 12:51:26 +0400 Subject: [PATCH 277/906] =?UTF-8?q?=F0=9F=91=B7=20Update=20CI=20to=20build?= =?UTF-8?q?=20MkDocs=20Insiders=20only=20when=20the=20secrets=20are=20avai?= =?UTF-8?q?lable,=20for=20Dependabot=20(#683)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 879cc7c5d8..3d29204f78 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -63,17 +63,17 @@ jobs: if: steps.cache.outputs.cache-hit != 'true' run: python -m poetry install - name: Install Material for MkDocs Insiders - if: ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false ) && steps.cache.outputs.cache-hit != 'true' + if: ( github.event_name != 'pull_request' || github.secret_source == 'Actions' ) && steps.cache.outputs.cache-hit != 'true' run: python -m poetry run pip install git+https://${{ secrets.SQLMODEL_MKDOCS_MATERIAL_INSIDERS }}@github.com/squidfunk/mkdocs-material-insiders.git - uses: actions/cache@v3 with: key: mkdocs-cards-${{ github.ref }} path: .cache - name: Build Docs - if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true + if: github.event_name == 'pull_request' && github.secret_source != 'Actions' run: python -m poetry run mkdocs build - name: Build Docs with Insiders - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false + if: github.event_name != 'pull_request' || github.secret_source == 'Actions' run: python -m poetry run mkdocs build --config-file mkdocs.insiders.yml - uses: actions/upload-artifact@v3 with: From a2d1b4eeafabec7902be19752109cb765c0e0697 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 29 Oct 2023 08:52:01 +0000 Subject: [PATCH 278/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index acff20b4d3..145cd025a2 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Update CI to build MkDocs Insiders only when the secrets are available, for Dependabot. PR [#683](https://github.com/tiangolo/sqlmodel/pull/683) by [@tiangolo](https://github.com/tiangolo). * ✨ Add support for passing a custom SQLAlchemy type to `Field()` with `sa_type`. PR [#505](https://github.com/tiangolo/sqlmodel/pull/505) by [@maru0123-2004](https://github.com/maru0123-2004). * 🎨 Update inline source examples, hide `#` in annotations (from MkDocs Material). PR [#677](https://github.com/tiangolo/sqlmodel/pull/677) by [@Matthieu-LAURENT39](https://github.com/Matthieu-LAURENT39). * ✨ Do not allow invalid combinations of field parameters for columns and relationships, `sa_column` excludes `sa_column_args`, `primary_key`, `nullable`, etc.. PR [#681](https://github.com/tiangolo/sqlmodel/pull/681) by [@tiangolo](https://github.com/tiangolo). From fc1d7afae995d49a9d1965390ffd6786c4e22e1d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 29 Oct 2023 08:55:04 +0000 Subject: [PATCH 279/906] =?UTF-8?q?=E2=AC=86=20Update=20black=20requiremen?= =?UTF-8?q?t=20from=20^22.10.0=20to=20>=3D22.10,<24.0=20(#664)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates the requirements on [black](https://github.com/psf/black) to permit the latest version. - [Release notes](https://github.com/psf/black/releases) - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) - [Commits](https://github.com/psf/black/compare/22.10.0...23.3.0) --- updated-dependencies: - dependency-name: black dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 20188513c1..0538aa1844 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ sqlalchemy2-stubs = {version = "*", allow-prereleases = true} pytest = "^7.0.1" mypy = "0.971" # Needed by the code generator using templates -black = "^22.10.0" +black = ">=22.10,<24.0" mkdocs-material = "9.1.21" pillow = "^9.3.0" cairosvg = "^2.5.2" From df1efcfa7f5370067c8c1aebfbc464f04ce61e4d Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 29 Oct 2023 08:55:43 +0000 Subject: [PATCH 280/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 145cd025a2..6da5f542ae 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Update black requirement from ^22.10.0 to >=22.10,<24.0. PR [#664](https://github.com/tiangolo/sqlmodel/pull/664) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Update CI to build MkDocs Insiders only when the secrets are available, for Dependabot. PR [#683](https://github.com/tiangolo/sqlmodel/pull/683) by [@tiangolo](https://github.com/tiangolo). * ✨ Add support for passing a custom SQLAlchemy type to `Field()` with `sa_type`. PR [#505](https://github.com/tiangolo/sqlmodel/pull/505) by [@maru0123-2004](https://github.com/maru0123-2004). * 🎨 Update inline source examples, hide `#` in annotations (from MkDocs Material). PR [#677](https://github.com/tiangolo/sqlmodel/pull/677) by [@Matthieu-LAURENT39](https://github.com/Matthieu-LAURENT39). From fa5d14e41359d0aef538ded971187856c529a147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 29 Oct 2023 12:59:56 +0400 Subject: [PATCH 281/906] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20mypy=20m?= =?UTF-8?q?anually=20(#684)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0538aa1844..22a845e8a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ sqlalchemy2-stubs = {version = "*", allow-prereleases = true} [tool.poetry.group.dev.dependencies] pytest = "^7.0.1" -mypy = "0.971" +mypy = "1.4.1" # Needed by the code generator using templates black = ">=22.10,<24.0" mkdocs-material = "9.1.21" From 9dd11ef74f4ee3e2bce37c231b362c151dbd89b6 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 29 Oct 2023 09:00:31 +0000 Subject: [PATCH 282/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 6da5f542ae..6a70fb1e9d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆️ Upgrade mypy manually. PR [#684](https://github.com/tiangolo/sqlmodel/pull/684) by [@tiangolo](https://github.com/tiangolo). * ⬆ Update black requirement from ^22.10.0 to >=22.10,<24.0. PR [#664](https://github.com/tiangolo/sqlmodel/pull/664) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Update CI to build MkDocs Insiders only when the secrets are available, for Dependabot. PR [#683](https://github.com/tiangolo/sqlmodel/pull/683) by [@tiangolo](https://github.com/tiangolo). * ✨ Add support for passing a custom SQLAlchemy type to `Field()` with `sa_type`. PR [#505](https://github.com/tiangolo/sqlmodel/pull/505) by [@maru0123-2004](https://github.com/maru0123-2004). From 4bb99763b19cb7a9c9d6f9d28a1e3b44573101cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 29 Oct 2023 13:08:22 +0400 Subject: [PATCH 283/906] =?UTF-8?q?=E2=AC=86=20Update=20mkdocs-material=20?= =?UTF-8?q?requirement=20from=209.1.21=20to=209.2.7=20(#675)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates the requirements on [mkdocs-material](https://github.com/squidfunk/mkdocs-material) to permit the latest version. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.1.21...9.2.7) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 22a845e8a8..cc7643165d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ pytest = "^7.0.1" mypy = "1.4.1" # Needed by the code generator using templates black = ">=22.10,<24.0" -mkdocs-material = "9.1.21" +mkdocs-material = "9.2.7" pillow = "^9.3.0" cairosvg = "^2.5.2" mdx-include = "^1.4.1" From 12af94655b742c8128ad609c900675cd71d3a747 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 29 Oct 2023 09:09:02 +0000 Subject: [PATCH 284/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 6a70fb1e9d..f21a451947 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Update mkdocs-material requirement from 9.1.21 to 9.2.7. PR [#675](https://github.com/tiangolo/sqlmodel/pull/675) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆️ Upgrade mypy manually. PR [#684](https://github.com/tiangolo/sqlmodel/pull/684) by [@tiangolo](https://github.com/tiangolo). * ⬆ Update black requirement from ^22.10.0 to >=22.10,<24.0. PR [#664](https://github.com/tiangolo/sqlmodel/pull/664) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Update CI to build MkDocs Insiders only when the secrets are available, for Dependabot. PR [#683](https://github.com/tiangolo/sqlmodel/pull/683) by [@tiangolo](https://github.com/tiangolo). From 188f7cd172687621b1302718d5adf4404695c5c3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 29 Oct 2023 09:18:26 +0000 Subject: [PATCH 285/906] =?UTF-8?q?=E2=AC=86=20Update=20coverage=20require?= =?UTF-8?q?ment=20from=20^6.2=20to=20>=3D6.2,<8.0=20(#663)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates the requirements on [coverage](https://github.com/nedbat/coveragepy) to permit the latest version. - [Release notes](https://github.com/nedbat/coveragepy/releases) - [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst) - [Commits](https://github.com/nedbat/coveragepy/compare/6.2...7.2.7) --- updated-dependencies: - dependency-name: coverage dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cc7643165d..23fa79bf31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ mkdocs-material = "9.2.7" pillow = "^9.3.0" cairosvg = "^2.5.2" mdx-include = "^1.4.1" -coverage = {extras = ["toml"], version = "^6.2"} +coverage = {extras = ["toml"], version = ">=6.2,<8.0"} fastapi = "^0.68.1" requests = "^2.26.0" ruff = "^0.1.2" From d8a20d9c21b53b91bda876a97ac56c27ae3f9579 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 29 Oct 2023 09:21:52 +0000 Subject: [PATCH 286/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index f21a451947..45c68fa2f7 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Update coverage requirement from ^6.2 to >=6.2,<8.0. PR [#663](https://github.com/tiangolo/sqlmodel/pull/663) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Update mkdocs-material requirement from 9.1.21 to 9.2.7. PR [#675](https://github.com/tiangolo/sqlmodel/pull/675) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆️ Upgrade mypy manually. PR [#684](https://github.com/tiangolo/sqlmodel/pull/684) by [@tiangolo](https://github.com/tiangolo). * ⬆ Update black requirement from ^22.10.0 to >=22.10,<24.0. PR [#664](https://github.com/tiangolo/sqlmodel/pull/664) by [@dependabot[bot]](https://github.com/apps/dependabot). From 0b2d015fc4ae506179b83b15bf25594b134b902f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 29 Oct 2023 13:56:00 +0400 Subject: [PATCH 287/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 45c68fa2f7..1cb36ed5f3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,14 +2,24 @@ ## Latest Changes +### Features + +* ✨ Add support for passing a custom SQLAlchemy type to `Field()` with `sa_type`. PR [#505](https://github.com/tiangolo/sqlmodel/pull/505) by [@maru0123-2004](https://github.com/maru0123-2004). + * You might consider this a breaking change if you were using an incompatible combination of arguments, those arguments were not taking effect and now you will have a type error and runtime error telling you that. +* ✨ Do not allow invalid combinations of field parameters for columns and relationships, `sa_column` excludes `sa_column_args`, `primary_key`, `nullable`, etc. PR [#681](https://github.com/tiangolo/sqlmodel/pull/681) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* 🎨 Update inline source examples, hide `#` in annotations (from MkDocs Material). PR [#677](https://github.com/tiangolo/sqlmodel/pull/677) by [@Matthieu-LAURENT39](https://github.com/Matthieu-LAURENT39). + +### Internal + * ⬆ Update coverage requirement from ^6.2 to >=6.2,<8.0. PR [#663](https://github.com/tiangolo/sqlmodel/pull/663) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Update mkdocs-material requirement from 9.1.21 to 9.2.7. PR [#675](https://github.com/tiangolo/sqlmodel/pull/675) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆️ Upgrade mypy manually. PR [#684](https://github.com/tiangolo/sqlmodel/pull/684) by [@tiangolo](https://github.com/tiangolo). * ⬆ Update black requirement from ^22.10.0 to >=22.10,<24.0. PR [#664](https://github.com/tiangolo/sqlmodel/pull/664) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Update CI to build MkDocs Insiders only when the secrets are available, for Dependabot. PR [#683](https://github.com/tiangolo/sqlmodel/pull/683) by [@tiangolo](https://github.com/tiangolo). -* ✨ Add support for passing a custom SQLAlchemy type to `Field()` with `sa_type`. PR [#505](https://github.com/tiangolo/sqlmodel/pull/505) by [@maru0123-2004](https://github.com/maru0123-2004). -* 🎨 Update inline source examples, hide `#` in annotations (from MkDocs Material). PR [#677](https://github.com/tiangolo/sqlmodel/pull/677) by [@Matthieu-LAURENT39](https://github.com/Matthieu-LAURENT39). -* ✨ Do not allow invalid combinations of field parameters for columns and relationships, `sa_column` excludes `sa_column_args`, `primary_key`, `nullable`, etc.. PR [#681](https://github.com/tiangolo/sqlmodel/pull/681) by [@tiangolo](https://github.com/tiangolo). + ## 0.0.10 ### Features From dacc1fa9ca9f1b71f768dd8dd196650d909255fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 29 Oct 2023 13:56:39 +0400 Subject: [PATCH 288/906] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.0.?= =?UTF-8?q?11?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 3 +++ sqlmodel/__init__.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 1cb36ed5f3..fa1525ce85 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.0.11 + ### Features * ✨ Add support for passing a custom SQLAlchemy type to `Field()` with `sa_type`. PR [#505](https://github.com/tiangolo/sqlmodel/pull/505) by [@maru0123-2004](https://github.com/maru0123-2004). diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index 6b65860d30..495ac9c8a8 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.0.10" +__version__ = "0.0.11" # Re-export from SQLAlchemy from sqlalchemy.engine import create_mock_engine as create_mock_engine From ed22232dee1753050c21c9b0141ccf627771c1e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 4 Nov 2023 05:42:52 +0400 Subject: [PATCH 289/906] =?UTF-8?q?=F0=9F=91=B7=20Upgrade=20latest-changes?= =?UTF-8?q?=20GitHub=20Action=20(#693)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/latest-changes.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml index 7e8403972d..f77dffc074 100644 --- a/.github/workflows/latest-changes.yml +++ b/.github/workflows/latest-changes.yml @@ -30,9 +30,12 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true - - uses: docker://tiangolo/latest-changes:0.0.3 + - uses: docker://tiangolo/latest-changes:0.2.0 + # - uses: tiangolo/latest-changes@main with: token: ${{ secrets.GITHUB_TOKEN }} latest_changes_file: docs/release-notes.md - latest_changes_header: '## Latest Changes\n\n' + latest_changes_header: '## Latest Changes' + end_regex: '^## ' debug_logs: true + label_header_prefix: '### ' From e05eae6a49515992b96f70bebee0a1abf5546a15 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 4 Nov 2023 01:43:16 +0000 Subject: [PATCH 290/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index fa1525ce85..fca595a49a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes +### Internal + +* 👷 Upgrade latest-changes GitHub Action. PR [#693](https://github.com/tiangolo/sqlmodel/pull/693) by [@tiangolo](https://github.com/tiangolo). ## 0.0.11 From c0294423f4364c59ccb8cf4279b100cf29e04794 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 8 Nov 2023 17:04:24 +0100 Subject: [PATCH 291/906] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#686)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - https://github.com/charliermarsh/ruff-pre-commit → https://github.com/astral-sh/ruff-pre-commit - [github.com/astral-sh/ruff-pre-commit: v0.1.2 → v0.1.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.2...v0.1.4) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 61aaf71411..6e84cce283 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,8 +13,8 @@ repos: - --unsafe - id: end-of-file-fixer - id: trailing-whitespace -- repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.1.2 +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.4 hooks: - id: ruff args: From 77c6fed305962b8ffd8e9277bc601efdcd499973 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 8 Nov 2023 16:04:47 +0000 Subject: [PATCH 292/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index fca595a49a..819f97244e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#686](https://github.com/tiangolo/sqlmodel/pull/686) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * 👷 Upgrade latest-changes GitHub Action. PR [#693](https://github.com/tiangolo/sqlmodel/pull/693) by [@tiangolo](https://github.com/tiangolo). ## 0.0.11 From 8ed856d32259c21297831f59a9be7ce90ceb6f4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 18 Nov 2023 12:30:37 +0100 Subject: [PATCH 293/906] =?UTF-8?q?=E2=9C=A8=20Upgrade=20SQLAlchemy=20to?= =?UTF-8?q?=202.0,=20including=20initial=20work=20by=20farahats9=20(#700)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mohamed Farahat Co-authored-by: Stefan Borer Co-authored-by: Peter Landry --- .github/workflows/test.yml | 2 + pyproject.toml | 14 +- scripts/generate_select.py | 6 +- sqlmodel/__init__.py | 53 ++- sqlmodel/engine/__init__.py | 0 sqlmodel/engine/create.py | 139 ------ sqlmodel/engine/result.py | 79 ---- sqlmodel/ext/asyncio/session.py | 144 ++++-- sqlmodel/main.py | 27 +- sqlmodel/orm/session.py | 109 +++-- sqlmodel/sql/expression.py | 428 +++++++++++++----- sqlmodel/sql/expression.py.jinja2 | 255 ++++++++++- sqlmodel/sql/sqltypes.py | 8 +- .../test_delete/test_tutorial001.py | 6 +- .../test_limit_and_offset/test_tutorial001.py | 6 +- .../test_multiple_models/test_tutorial001.py | 4 +- .../test_multiple_models/test_tutorial002.py | 4 +- .../test_read_one/test_tutorial001.py | 4 +- .../test_relationships/test_tutorial001.py | 6 +- .../test_response_model/test_tutorial001.py | 4 +- .../test_tutorial001.py | 6 +- .../test_simple_hero_api/test_tutorial001.py | 4 +- .../test_teams/test_tutorial001.py | 6 +- .../test_update/test_tutorial001.py | 6 +- 24 files changed, 809 insertions(+), 511 deletions(-) delete mode 100644 sqlmodel/engine/__init__.py delete mode 100644 sqlmodel/engine/create.py delete mode 100644 sqlmodel/engine/result.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 201abc7c22..c3b07f484e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -56,6 +56,8 @@ jobs: if: steps.cache.outputs.cache-hit != 'true' run: python -m poetry install - name: Lint + # Do not run on Python 3.7 as mypy behaves differently + if: matrix.python-version != '3.7' run: python -m poetry run bash scripts/lint.sh - run: mkdir coverage - name: Test diff --git a/pyproject.toml b/pyproject.toml index 23fa79bf31..515bbaf66c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,9 +31,8 @@ classifiers = [ [tool.poetry.dependencies] python = "^3.7" -SQLAlchemy = ">=1.4.36,<2.0.0" +SQLAlchemy = ">=2.0.0,<2.1.0" pydantic = "^1.9.0" -sqlalchemy2-stubs = {version = "*", allow-prereleases = true} [tool.poetry.group.dev.dependencies] pytest = "^7.0.1" @@ -45,9 +44,10 @@ pillow = "^9.3.0" cairosvg = "^2.5.2" mdx-include = "^1.4.1" coverage = {extras = ["toml"], version = ">=6.2,<8.0"} -fastapi = "^0.68.1" -requests = "^2.26.0" +fastapi = "^0.103.2" ruff = "^0.1.2" +# For FastAPI tests +httpx = "0.24.1" [build-system] requires = ["poetry-core"] @@ -80,6 +80,12 @@ strict = true module = "sqlmodel.sql.expression" warn_unused_ignores = false +[[tool.mypy.overrides]] +module = "docs_src.*" +disallow_incomplete_defs = false +disallow_untyped_defs = false +disallow_untyped_calls = false + [tool.ruff] select = [ "E", # pycodestyle errors diff --git a/scripts/generate_select.py b/scripts/generate_select.py index f8aa30023f..88e0e0a997 100644 --- a/scripts/generate_select.py +++ b/scripts/generate_select.py @@ -34,9 +34,9 @@ class Arg(BaseModel): arg = Arg(name=f"entity_{i}", annotation=t_var) ret_type = t_var else: - t_type = f"_TModel_{i}" - t_var = f"Type[{t_type}]" - arg = Arg(name=f"entity_{i}", annotation=t_var) + t_type = f"_T{i}" + t_var = f"_TCCA[{t_type}]" + arg = Arg(name=f"__ent{i}", annotation=t_var) ret_type = t_type args.append(arg) return_types.append(ret_type) diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index 495ac9c8a8..e943257165 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,9 +1,12 @@ __version__ = "0.0.11" # Re-export from SQLAlchemy +from sqlalchemy.engine import create_engine as create_engine from sqlalchemy.engine import create_mock_engine as create_mock_engine from sqlalchemy.engine import engine_from_config as engine_from_config from sqlalchemy.inspection import inspect as inspect +from sqlalchemy.pool import QueuePool as QueuePool +from sqlalchemy.pool import StaticPool as StaticPool from sqlalchemy.schema import BLANK_SCHEMA as BLANK_SCHEMA from sqlalchemy.schema import DDL as DDL from sqlalchemy.schema import CheckConstraint as CheckConstraint @@ -21,7 +24,6 @@ from sqlalchemy.schema import PrimaryKeyConstraint as PrimaryKeyConstraint from sqlalchemy.schema import Sequence as Sequence from sqlalchemy.schema import Table as Table -from sqlalchemy.schema import ThreadLocalMetaData as ThreadLocalMetaData from sqlalchemy.schema import UniqueConstraint as UniqueConstraint from sqlalchemy.sql import LABEL_STYLE_DEFAULT as LABEL_STYLE_DEFAULT from sqlalchemy.sql import ( @@ -32,26 +34,14 @@ LABEL_STYLE_TABLENAME_PLUS_COL as LABEL_STYLE_TABLENAME_PLUS_COL, ) from sqlalchemy.sql import alias as alias -from sqlalchemy.sql import all_ as all_ -from sqlalchemy.sql import and_ as and_ -from sqlalchemy.sql import any_ as any_ -from sqlalchemy.sql import asc as asc -from sqlalchemy.sql import between as between from sqlalchemy.sql import bindparam as bindparam -from sqlalchemy.sql import case as case -from sqlalchemy.sql import cast as cast -from sqlalchemy.sql import collate as collate from sqlalchemy.sql import column as column from sqlalchemy.sql import delete as delete -from sqlalchemy.sql import desc as desc -from sqlalchemy.sql import distinct as distinct from sqlalchemy.sql import except_ as except_ from sqlalchemy.sql import except_all as except_all from sqlalchemy.sql import exists as exists -from sqlalchemy.sql import extract as extract from sqlalchemy.sql import false as false from sqlalchemy.sql import func as func -from sqlalchemy.sql import funcfilter as funcfilter from sqlalchemy.sql import insert as insert from sqlalchemy.sql import intersect as intersect from sqlalchemy.sql import intersect_all as intersect_all @@ -61,28 +51,19 @@ from sqlalchemy.sql import literal as literal from sqlalchemy.sql import literal_column as literal_column from sqlalchemy.sql import modifier as modifier -from sqlalchemy.sql import not_ as not_ from sqlalchemy.sql import null as null -from sqlalchemy.sql import nulls_first as nulls_first -from sqlalchemy.sql import nulls_last as nulls_last from sqlalchemy.sql import nullsfirst as nullsfirst from sqlalchemy.sql import nullslast as nullslast -from sqlalchemy.sql import or_ as or_ from sqlalchemy.sql import outerjoin as outerjoin from sqlalchemy.sql import outparam as outparam -from sqlalchemy.sql import over as over -from sqlalchemy.sql import subquery as subquery from sqlalchemy.sql import table as table from sqlalchemy.sql import tablesample as tablesample from sqlalchemy.sql import text as text from sqlalchemy.sql import true as true -from sqlalchemy.sql import tuple_ as tuple_ -from sqlalchemy.sql import type_coerce as type_coerce from sqlalchemy.sql import union as union from sqlalchemy.sql import union_all as union_all from sqlalchemy.sql import update as update from sqlalchemy.sql import values as values -from sqlalchemy.sql import within_group as within_group from sqlalchemy.types import ARRAY as ARRAY from sqlalchemy.types import BIGINT as BIGINT from sqlalchemy.types import BINARY as BINARY @@ -93,6 +74,8 @@ from sqlalchemy.types import DATE as DATE from sqlalchemy.types import DATETIME as DATETIME from sqlalchemy.types import DECIMAL as DECIMAL +from sqlalchemy.types import DOUBLE as DOUBLE +from sqlalchemy.types import DOUBLE_PRECISION as DOUBLE_PRECISION from sqlalchemy.types import FLOAT as FLOAT from sqlalchemy.types import INT as INT from sqlalchemy.types import INTEGER as INTEGER @@ -105,12 +88,14 @@ from sqlalchemy.types import TEXT as TEXT from sqlalchemy.types import TIME as TIME from sqlalchemy.types import TIMESTAMP as TIMESTAMP +from sqlalchemy.types import UUID as UUID from sqlalchemy.types import VARBINARY as VARBINARY from sqlalchemy.types import VARCHAR as VARCHAR from sqlalchemy.types import BigInteger as BigInteger from sqlalchemy.types import Boolean as Boolean from sqlalchemy.types import Date as Date from sqlalchemy.types import DateTime as DateTime +from sqlalchemy.types import Double as Double from sqlalchemy.types import Enum as Enum from sqlalchemy.types import Float as Float from sqlalchemy.types import Integer as Integer @@ -122,16 +107,38 @@ from sqlalchemy.types import String as String from sqlalchemy.types import Text as Text from sqlalchemy.types import Time as Time +from sqlalchemy.types import TupleType as TupleType from sqlalchemy.types import TypeDecorator as TypeDecorator from sqlalchemy.types import Unicode as Unicode from sqlalchemy.types import UnicodeText as UnicodeText +from sqlalchemy.types import Uuid as Uuid # From SQLModel, modifications of SQLAlchemy or equivalents of Pydantic -from .engine.create import create_engine as create_engine from .main import Field as Field from .main import Relationship as Relationship from .main import SQLModel as SQLModel from .orm.session import Session as Session +from .sql.expression import all_ as all_ +from .sql.expression import and_ as and_ +from .sql.expression import any_ as any_ +from .sql.expression import asc as asc +from .sql.expression import between as between +from .sql.expression import case as case +from .sql.expression import cast as cast from .sql.expression import col as col +from .sql.expression import collate as collate +from .sql.expression import desc as desc +from .sql.expression import distinct as distinct +from .sql.expression import extract as extract +from .sql.expression import funcfilter as funcfilter +from .sql.expression import not_ as not_ +from .sql.expression import nulls_first as nulls_first +from .sql.expression import nulls_last as nulls_last +from .sql.expression import or_ as or_ +from .sql.expression import over as over from .sql.expression import select as select +from .sql.expression import tuple_ as tuple_ +from .sql.expression import type_coerce as type_coerce +from .sql.expression import within_group as within_group +from .sql.sqltypes import GUID as GUID from .sql.sqltypes import AutoString as AutoString diff --git a/sqlmodel/engine/__init__.py b/sqlmodel/engine/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/sqlmodel/engine/create.py b/sqlmodel/engine/create.py deleted file mode 100644 index b2d567b1b1..0000000000 --- a/sqlmodel/engine/create.py +++ /dev/null @@ -1,139 +0,0 @@ -import json -import sqlite3 -from typing import Any, Callable, Dict, List, Optional, Type, Union - -from sqlalchemy import create_engine as _create_engine -from sqlalchemy.engine.url import URL -from sqlalchemy.future import Engine as _FutureEngine -from sqlalchemy.pool import Pool -from typing_extensions import Literal, TypedDict - -from ..default import Default, _DefaultPlaceholder - -# Types defined in sqlalchemy2-stubs, but can't be imported, so re-define here - -_Debug = Literal["debug"] - -_IsolationLevel = Literal[ - "SERIALIZABLE", - "REPEATABLE READ", - "READ COMMITTED", - "READ UNCOMMITTED", - "AUTOCOMMIT", -] -_ParamStyle = Literal["qmark", "numeric", "named", "format", "pyformat"] -_ResetOnReturn = Literal["rollback", "commit"] - - -class _SQLiteConnectArgs(TypedDict, total=False): - timeout: float - detect_types: Any - isolation_level: Optional[Literal["DEFERRED", "IMMEDIATE", "EXCLUSIVE"]] - check_same_thread: bool - factory: Type[sqlite3.Connection] - cached_statements: int - uri: bool - - -_ConnectArgs = Union[_SQLiteConnectArgs, Dict[str, Any]] - - -# Re-define create_engine to have by default future=True, and assume that's what is used -# Also show the default values used for each parameter, but don't set them unless -# explicitly passed as arguments by the user to prevent errors. E.g. SQLite doesn't -# support pool connection arguments. -def create_engine( - url: Union[str, URL], - *, - connect_args: _ConnectArgs = Default({}), # type: ignore - echo: Union[bool, _Debug] = Default(False), - echo_pool: Union[bool, _Debug] = Default(False), - enable_from_linting: bool = Default(True), - encoding: str = Default("utf-8"), - execution_options: Dict[Any, Any] = Default({}), - future: bool = True, - hide_parameters: bool = Default(False), - implicit_returning: bool = Default(True), - isolation_level: Optional[_IsolationLevel] = Default(None), - json_deserializer: Callable[..., Any] = Default(json.loads), - json_serializer: Callable[..., Any] = Default(json.dumps), - label_length: Optional[int] = Default(None), - logging_name: Optional[str] = Default(None), - max_identifier_length: Optional[int] = Default(None), - max_overflow: int = Default(10), - module: Optional[Any] = Default(None), - paramstyle: Optional[_ParamStyle] = Default(None), - pool: Optional[Pool] = Default(None), - poolclass: Optional[Type[Pool]] = Default(None), - pool_logging_name: Optional[str] = Default(None), - pool_pre_ping: bool = Default(False), - pool_size: int = Default(5), - pool_recycle: int = Default(-1), - pool_reset_on_return: Optional[_ResetOnReturn] = Default("rollback"), - pool_timeout: float = Default(30), - pool_use_lifo: bool = Default(False), - plugins: Optional[List[str]] = Default(None), - query_cache_size: Optional[int] = Default(None), - **kwargs: Any, -) -> _FutureEngine: - current_kwargs: Dict[str, Any] = { - "future": future, - } - if not isinstance(echo, _DefaultPlaceholder): - current_kwargs["echo"] = echo - if not isinstance(echo_pool, _DefaultPlaceholder): - current_kwargs["echo_pool"] = echo_pool - if not isinstance(enable_from_linting, _DefaultPlaceholder): - current_kwargs["enable_from_linting"] = enable_from_linting - if not isinstance(connect_args, _DefaultPlaceholder): - current_kwargs["connect_args"] = connect_args - if not isinstance(encoding, _DefaultPlaceholder): - current_kwargs["encoding"] = encoding - if not isinstance(execution_options, _DefaultPlaceholder): - current_kwargs["execution_options"] = execution_options - if not isinstance(hide_parameters, _DefaultPlaceholder): - current_kwargs["hide_parameters"] = hide_parameters - if not isinstance(implicit_returning, _DefaultPlaceholder): - current_kwargs["implicit_returning"] = implicit_returning - if not isinstance(isolation_level, _DefaultPlaceholder): - current_kwargs["isolation_level"] = isolation_level - if not isinstance(json_deserializer, _DefaultPlaceholder): - current_kwargs["json_deserializer"] = json_deserializer - if not isinstance(json_serializer, _DefaultPlaceholder): - current_kwargs["json_serializer"] = json_serializer - if not isinstance(label_length, _DefaultPlaceholder): - current_kwargs["label_length"] = label_length - if not isinstance(logging_name, _DefaultPlaceholder): - current_kwargs["logging_name"] = logging_name - if not isinstance(max_identifier_length, _DefaultPlaceholder): - current_kwargs["max_identifier_length"] = max_identifier_length - if not isinstance(max_overflow, _DefaultPlaceholder): - current_kwargs["max_overflow"] = max_overflow - if not isinstance(module, _DefaultPlaceholder): - current_kwargs["module"] = module - if not isinstance(paramstyle, _DefaultPlaceholder): - current_kwargs["paramstyle"] = paramstyle - if not isinstance(pool, _DefaultPlaceholder): - current_kwargs["pool"] = pool - if not isinstance(poolclass, _DefaultPlaceholder): - current_kwargs["poolclass"] = poolclass - if not isinstance(pool_logging_name, _DefaultPlaceholder): - current_kwargs["pool_logging_name"] = pool_logging_name - if not isinstance(pool_pre_ping, _DefaultPlaceholder): - current_kwargs["pool_pre_ping"] = pool_pre_ping - if not isinstance(pool_size, _DefaultPlaceholder): - current_kwargs["pool_size"] = pool_size - if not isinstance(pool_recycle, _DefaultPlaceholder): - current_kwargs["pool_recycle"] = pool_recycle - if not isinstance(pool_reset_on_return, _DefaultPlaceholder): - current_kwargs["pool_reset_on_return"] = pool_reset_on_return - if not isinstance(pool_timeout, _DefaultPlaceholder): - current_kwargs["pool_timeout"] = pool_timeout - if not isinstance(pool_use_lifo, _DefaultPlaceholder): - current_kwargs["pool_use_lifo"] = pool_use_lifo - if not isinstance(plugins, _DefaultPlaceholder): - current_kwargs["plugins"] = plugins - if not isinstance(query_cache_size, _DefaultPlaceholder): - current_kwargs["query_cache_size"] = query_cache_size - current_kwargs.update(kwargs) - return _create_engine(url, **current_kwargs) # type: ignore diff --git a/sqlmodel/engine/result.py b/sqlmodel/engine/result.py deleted file mode 100644 index 7a25422227..0000000000 --- a/sqlmodel/engine/result.py +++ /dev/null @@ -1,79 +0,0 @@ -from typing import Generic, Iterator, List, Optional, TypeVar - -from sqlalchemy.engine.result import Result as _Result -from sqlalchemy.engine.result import ScalarResult as _ScalarResult - -_T = TypeVar("_T") - - -class ScalarResult(_ScalarResult, Generic[_T]): - def all(self) -> List[_T]: - return super().all() - - def partitions(self, size: Optional[int] = None) -> Iterator[List[_T]]: - return super().partitions(size) - - def fetchall(self) -> List[_T]: - return super().fetchall() - - def fetchmany(self, size: Optional[int] = None) -> List[_T]: - return super().fetchmany(size) - - def __iter__(self) -> Iterator[_T]: - return super().__iter__() - - def __next__(self) -> _T: - return super().__next__() # type: ignore - - def first(self) -> Optional[_T]: - return super().first() - - def one_or_none(self) -> Optional[_T]: - return super().one_or_none() - - def one(self) -> _T: - return super().one() # type: ignore - - -class Result(_Result, Generic[_T]): - def scalars(self, index: int = 0) -> ScalarResult[_T]: - return super().scalars(index) # type: ignore - - def __iter__(self) -> Iterator[_T]: # type: ignore - return super().__iter__() # type: ignore - - def __next__(self) -> _T: # type: ignore - return super().__next__() # type: ignore - - def partitions(self, size: Optional[int] = None) -> Iterator[List[_T]]: # type: ignore - return super().partitions(size) # type: ignore - - def fetchall(self) -> List[_T]: # type: ignore - return super().fetchall() # type: ignore - - def fetchone(self) -> Optional[_T]: # type: ignore - return super().fetchone() # type: ignore - - def fetchmany(self, size: Optional[int] = None) -> List[_T]: # type: ignore - return super().fetchmany() # type: ignore - - def all(self) -> List[_T]: # type: ignore - return super().all() # type: ignore - - def first(self) -> Optional[_T]: # type: ignore - return super().first() # type: ignore - - def one_or_none(self) -> Optional[_T]: # type: ignore - return super().one_or_none() # type: ignore - - def scalar_one(self) -> _T: - return super().scalar_one() # type: ignore - - def scalar_one_or_none(self) -> Optional[_T]: - return super().scalar_one_or_none() - - def one(self) -> _T: # type: ignore - return super().one() # type: ignore - - def scalar(self) -> Optional[_T]: - return super().scalar() diff --git a/sqlmodel/ext/asyncio/session.py b/sqlmodel/ext/asyncio/session.py index f500c44dc2..012d8ef5e4 100644 --- a/sqlmodel/ext/asyncio/session.py +++ b/sqlmodel/ext/asyncio/session.py @@ -1,45 +1,38 @@ -from typing import Any, Mapping, Optional, Sequence, TypeVar, Union, overload +from typing import ( + Any, + Dict, + Mapping, + Optional, + Sequence, + Type, + TypeVar, + Union, + cast, + overload, +) from sqlalchemy import util +from sqlalchemy.engine.interfaces import _CoreAnyExecuteParams +from sqlalchemy.engine.result import Result, ScalarResult, TupleResult from sqlalchemy.ext.asyncio import AsyncSession as _AsyncSession -from sqlalchemy.ext.asyncio import engine -from sqlalchemy.ext.asyncio.engine import AsyncConnection, AsyncEngine +from sqlalchemy.ext.asyncio.result import _ensure_sync_result +from sqlalchemy.ext.asyncio.session import _EXECUTE_OPTIONS +from sqlalchemy.orm._typing import OrmExecuteOptionsParameter +from sqlalchemy.sql.base import Executable as _Executable from sqlalchemy.util.concurrency import greenlet_spawn +from typing_extensions import deprecated -from ...engine.result import Result, ScalarResult from ...orm.session import Session from ...sql.base import Executable from ...sql.expression import Select, SelectOfScalar -_TSelectParam = TypeVar("_TSelectParam") +_TSelectParam = TypeVar("_TSelectParam", bound=Any) class AsyncSession(_AsyncSession): + sync_session_class: Type[Session] = Session sync_session: Session - def __init__( - self, - bind: Optional[Union[AsyncConnection, AsyncEngine]] = None, - binds: Optional[Mapping[object, Union[AsyncConnection, AsyncEngine]]] = None, - **kw: Any, - ): - # All the same code of the original AsyncSession - kw["future"] = True - if bind: - self.bind = bind - bind = engine._get_sync_engine_or_connection(bind) # type: ignore - - if binds: - self.binds = binds - binds = { - key: engine._get_sync_engine_or_connection(b) # type: ignore - for key, b in binds.items() - } - - self.sync_session = self._proxied = self._assign_proxied( # type: ignore - Session(bind=bind, binds=binds, **kw) # type: ignore - ) - @overload async def exec( self, @@ -47,11 +40,10 @@ async def exec( *, params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, execution_options: Mapping[str, Any] = util.EMPTY_DICT, - bind_arguments: Optional[Mapping[str, Any]] = None, + bind_arguments: Optional[Dict[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, - **kw: Any, - ) -> Result[_TSelectParam]: + ) -> TupleResult[_TSelectParam]: ... @overload @@ -61,10 +53,9 @@ async def exec( *, params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, execution_options: Mapping[str, Any] = util.EMPTY_DICT, - bind_arguments: Optional[Mapping[str, Any]] = None, + bind_arguments: Optional[Dict[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, - **kw: Any, ) -> ScalarResult[_TSelectParam]: ... @@ -75,20 +66,87 @@ async def exec( SelectOfScalar[_TSelectParam], Executable[_TSelectParam], ], + *, params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, - execution_options: Mapping[Any, Any] = util.EMPTY_DICT, - bind_arguments: Optional[Mapping[str, Any]] = None, - **kw: Any, - ) -> Union[Result[_TSelectParam], ScalarResult[_TSelectParam]]: - # TODO: the documentation says execution_options accepts a dict, but only - # util.immutabledict has the union() method. Is this a bug in SQLAlchemy? - execution_options = execution_options.union({"prebuffer_rows": True}) # type: ignore - - return await greenlet_spawn( + execution_options: Mapping[str, Any] = util.EMPTY_DICT, + bind_arguments: Optional[Dict[str, Any]] = None, + _parent_execute_state: Optional[Any] = None, + _add_event: Optional[Any] = None, + ) -> Union[TupleResult[_TSelectParam], ScalarResult[_TSelectParam]]: + if execution_options: + execution_options = util.immutabledict(execution_options).union( + _EXECUTE_OPTIONS + ) + else: + execution_options = _EXECUTE_OPTIONS + + result = await greenlet_spawn( self.sync_session.exec, statement, params=params, execution_options=execution_options, bind_arguments=bind_arguments, - **kw, + _parent_execute_state=_parent_execute_state, + _add_event=_add_event, + ) + result_value = await _ensure_sync_result( + cast(Result[_TSelectParam], result), self.exec + ) + return result_value # type: ignore + + @deprecated( + """ + 🚨 You probably want to use `session.exec()` instead of `session.execute()`. + + This is the original SQLAlchemy `session.execute()` method that returns objects + of type `Row`, and that you have to call `scalars()` to get the model objects. + + For example: + + ```Python + heroes = await session.execute(select(Hero)).scalars().all() + ``` + + instead you could use `exec()`: + + ```Python + heroes = await session.exec(select(Hero)).all() + ``` + """ + ) + async def execute( # type: ignore + self, + statement: _Executable, + params: Optional[_CoreAnyExecuteParams] = None, + *, + execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT, + bind_arguments: Optional[Dict[str, Any]] = None, + _parent_execute_state: Optional[Any] = None, + _add_event: Optional[Any] = None, + ) -> Result[Any]: + """ + 🚨 You probably want to use `session.exec()` instead of `session.execute()`. + + This is the original SQLAlchemy `session.execute()` method that returns objects + of type `Row`, and that you have to call `scalars()` to get the model objects. + + For example: + + ```Python + heroes = await session.execute(select(Hero)).scalars().all() + ``` + + instead you could use `exec()`: + + ```Python + heroes = await session.exec(select(Hero)).all() + ``` + """ + return await super().execute( + statement, + params=params, + execution_options=execution_options, + bind_arguments=bind_arguments, + _parent_execute_state=_parent_execute_state, + _add_event=_add_event, ) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 2b69dd2a75..c30af5779f 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -45,12 +45,19 @@ inspect, ) from sqlalchemy import Enum as sa_Enum -from sqlalchemy.orm import RelationshipProperty, declared_attr, registry, relationship +from sqlalchemy.orm import ( + Mapped, + RelationshipProperty, + declared_attr, + registry, + relationship, +) from sqlalchemy.orm.attributes import set_attribute from sqlalchemy.orm.decl_api import DeclarativeMeta from sqlalchemy.orm.instrumentation import is_instrumented from sqlalchemy.sql.schema import MetaData from sqlalchemy.sql.sqltypes import LargeBinary, Time +from typing_extensions import get_origin from .sql.sqltypes import GUID, AutoString @@ -483,7 +490,16 @@ def __init__( # over anything else, use that and continue with the next attribute setattr(cls, rel_name, rel_info.sa_relationship) # Fix #315 continue - ann = cls.__annotations__[rel_name] + raw_ann = cls.__annotations__[rel_name] + origin = get_origin(raw_ann) + if origin is Mapped: + ann = raw_ann.__args__[0] + else: + ann = raw_ann + # Plain forward references, for models not yet defined, are not + # handled well by SQLAlchemy without Mapped, so, wrap the + # annotations in Mapped here + cls.__annotations__[rel_name] = Mapped[ann] # type: ignore[valid-type] temp_field = ModelField.infer( name=rel_name, value=rel_info, @@ -511,9 +527,7 @@ def __init__( rel_args.extend(rel_info.sa_relationship_args) if rel_info.sa_relationship_kwargs: rel_kwargs.update(rel_info.sa_relationship_kwargs) - rel_value: RelationshipProperty = relationship( # type: ignore - relationship_to, *rel_args, **rel_kwargs - ) + rel_value = relationship(relationship_to, *rel_args, **rel_kwargs) setattr(cls, rel_name, rel_value) # Fix #315 # SQLAlchemy no longer uses dict_ # Ref: https://github.com/sqlalchemy/sqlalchemy/commit/428ea01f00a9cc7f85e435018565eb6da7af1b77 @@ -642,6 +656,7 @@ class SQLModel(BaseModel, metaclass=SQLModelMetaclass, registry=default_registry __sqlmodel_relationships__: ClassVar[Dict[str, RelationshipProperty]] # type: ignore __name__: ClassVar[str] metadata: ClassVar[MetaData] + __allow_unmapped__ = True # https://docs.sqlalchemy.org/en/20/changelog/migration_20.html#migration-20-step-six class Config: orm_mode = True @@ -685,7 +700,7 @@ def __setattr__(self, name: str, value: Any) -> None: return else: # Set in SQLAlchemy, before Pydantic to trigger events and updates - if getattr(self.__config__, "table", False) and is_instrumented(self, name): + if getattr(self.__config__, "table", False) and is_instrumented(self, name): # type: ignore set_attribute(self, name, value) # Set in Pydantic model to trigger possible validation changes, only for # non relationship values diff --git a/sqlmodel/orm/session.py b/sqlmodel/orm/session.py index 0c70c290ae..6050d5fbc1 100644 --- a/sqlmodel/orm/session.py +++ b/sqlmodel/orm/session.py @@ -1,16 +1,27 @@ -from typing import Any, Mapping, Optional, Sequence, Type, TypeVar, Union, overload +from typing import ( + Any, + Dict, + Mapping, + Optional, + Sequence, + TypeVar, + Union, + overload, +) from sqlalchemy import util +from sqlalchemy.engine.interfaces import _CoreAnyExecuteParams +from sqlalchemy.engine.result import Result, ScalarResult, TupleResult from sqlalchemy.orm import Query as _Query from sqlalchemy.orm import Session as _Session +from sqlalchemy.orm._typing import OrmExecuteOptionsParameter +from sqlalchemy.sql._typing import _ColumnsClauseArgument from sqlalchemy.sql.base import Executable as _Executable -from typing_extensions import Literal +from sqlmodel.sql.base import Executable +from sqlmodel.sql.expression import Select, SelectOfScalar +from typing_extensions import deprecated -from ..engine.result import Result, ScalarResult -from ..sql.base import Executable -from ..sql.expression import Select, SelectOfScalar - -_TSelectParam = TypeVar("_TSelectParam") +_TSelectParam = TypeVar("_TSelectParam", bound=Any) class Session(_Session): @@ -21,11 +32,10 @@ def exec( *, params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, execution_options: Mapping[str, Any] = util.EMPTY_DICT, - bind_arguments: Optional[Mapping[str, Any]] = None, + bind_arguments: Optional[Dict[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, - **kw: Any, - ) -> Result[_TSelectParam]: + ) -> TupleResult[_TSelectParam]: ... @overload @@ -35,10 +45,9 @@ def exec( *, params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, execution_options: Mapping[str, Any] = util.EMPTY_DICT, - bind_arguments: Optional[Mapping[str, Any]] = None, + bind_arguments: Optional[Dict[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, - **kw: Any, ) -> ScalarResult[_TSelectParam]: ... @@ -52,11 +61,10 @@ def exec( *, params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, execution_options: Mapping[str, Any] = util.EMPTY_DICT, - bind_arguments: Optional[Mapping[str, Any]] = None, + bind_arguments: Optional[Dict[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, - **kw: Any, - ) -> Union[Result[_TSelectParam], ScalarResult[_TSelectParam]]: + ) -> Union[TupleResult[_TSelectParam], ScalarResult[_TSelectParam]]: results = super().execute( statement, params=params, @@ -64,21 +72,40 @@ def exec( bind_arguments=bind_arguments, _parent_execute_state=_parent_execute_state, _add_event=_add_event, - **kw, ) if isinstance(statement, SelectOfScalar): - return results.scalars() # type: ignore + return results.scalars() return results # type: ignore - def execute( + @deprecated( + """ + 🚨 You probably want to use `session.exec()` instead of `session.execute()`. + + This is the original SQLAlchemy `session.execute()` method that returns objects + of type `Row`, and that you have to call `scalars()` to get the model objects. + + For example: + + ```Python + heroes = session.execute(select(Hero)).scalars().all() + ``` + + instead you could use `exec()`: + + ```Python + heroes = session.exec(select(Hero)).all() + ``` + """ + ) + def execute( # type: ignore self, statement: _Executable, - params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, - execution_options: Optional[Mapping[str, Any]] = util.EMPTY_DICT, - bind_arguments: Optional[Mapping[str, Any]] = None, + params: Optional[_CoreAnyExecuteParams] = None, + *, + execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT, + bind_arguments: Optional[Dict[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, - **kw: Any, ) -> Result[Any]: """ 🚨 You probably want to use `session.exec()` instead of `session.execute()`. @@ -98,17 +125,16 @@ def execute( heroes = session.exec(select(Hero)).all() ``` """ - return super().execute( # type: ignore + return super().execute( statement, params=params, execution_options=execution_options, bind_arguments=bind_arguments, _parent_execute_state=_parent_execute_state, _add_event=_add_event, - **kw, ) - def query(self, *entities: Any, **kwargs: Any) -> "_Query[Any]": + @deprecated( """ 🚨 You probably want to use `session.exec()` instead of `session.query()`. @@ -118,24 +144,17 @@ def query(self, *entities: Any, **kwargs: Any) -> "_Query[Any]": Or otherwise you might want to use `session.execute()` instead of `session.query()`. """ - return super().query(*entities, **kwargs) + ) + def query( # type: ignore + self, *entities: _ColumnsClauseArgument[Any], **kwargs: Any + ) -> _Query[Any]: + """ + 🚨 You probably want to use `session.exec()` instead of `session.query()`. - def get( - self, - entity: Type[_TSelectParam], - ident: Any, - options: Optional[Sequence[Any]] = None, - populate_existing: bool = False, - with_for_update: Optional[Union[Literal[True], Mapping[str, Any]]] = None, - identity_token: Optional[Any] = None, - execution_options: Optional[Mapping[Any, Any]] = util.EMPTY_DICT, - ) -> Optional[_TSelectParam]: - return super().get( - entity, - ident, - options=options, - populate_existing=populate_existing, - with_for_update=with_for_update, - identity_token=identity_token, - execution_options=execution_options, - ) + `session.exec()` is SQLModel's own short version with increased type + annotations. + + Or otherwise you might want to use `session.execute()` instead of + `session.query()`. + """ + return super().query(*entities, **kwargs) diff --git a/sqlmodel/sql/expression.py b/sqlmodel/sql/expression.py index 264e39cba7..a8a572501c 100644 --- a/sqlmodel/sql/expression.py +++ b/sqlmodel/sql/expression.py @@ -2,10 +2,10 @@ from datetime import datetime from typing import ( - TYPE_CHECKING, Any, - Generic, + Iterable, Mapping, + Optional, Sequence, Tuple, Type, @@ -15,15 +15,223 @@ ) from uuid import UUID -from sqlalchemy import Column -from sqlalchemy.orm import InstrumentedAttribute -from sqlalchemy.sql.elements import ColumnClause +import sqlalchemy +from sqlalchemy import ( + Column, + ColumnElement, + Extract, + FunctionElement, + FunctionFilter, + Label, + Over, + TypeCoerce, + WithinGroup, +) +from sqlalchemy.orm import InstrumentedAttribute, Mapped +from sqlalchemy.sql._typing import ( + _ColumnExpressionArgument, + _ColumnExpressionOrLiteralArgument, + _ColumnExpressionOrStrLabelArgument, +) +from sqlalchemy.sql.elements import ( + BinaryExpression, + Case, + Cast, + CollectionAggregate, + ColumnClause, + SQLCoreOperations, + TryCast, + UnaryExpression, +) from sqlalchemy.sql.expression import Select as _Select +from sqlalchemy.sql.roles import TypedColumnsClauseRole +from sqlalchemy.sql.type_api import TypeEngine +from typing_extensions import Literal, Self + +_T = TypeVar("_T") + +_TypeEngineArgument = Union[Type[TypeEngine[_T]], TypeEngine[_T]] + +# Redefine operatos that would only take a column expresion to also take the (virtual) +# types of Pydantic models, e.g. str instead of only Mapped[str]. + + +def all_(expr: Union[_ColumnExpressionArgument[_T], _T]) -> CollectionAggregate[bool]: + return sqlalchemy.all_(expr) # type: ignore[arg-type] + + +def and_( + initial_clause: Union[Literal[True], _ColumnExpressionArgument[bool], bool], + *clauses: Union[_ColumnExpressionArgument[bool], bool], +) -> ColumnElement[bool]: + return sqlalchemy.and_(initial_clause, *clauses) # type: ignore[arg-type] + + +def any_(expr: Union[_ColumnExpressionArgument[_T], _T]) -> CollectionAggregate[bool]: + return sqlalchemy.any_(expr) # type: ignore[arg-type] + + +def asc( + column: Union[_ColumnExpressionOrStrLabelArgument[_T], _T], +) -> UnaryExpression[_T]: + return sqlalchemy.asc(column) # type: ignore[arg-type] + + +def collate( + expression: Union[_ColumnExpressionArgument[str], str], collation: str +) -> BinaryExpression[str]: + return sqlalchemy.collate(expression, collation) # type: ignore[arg-type] + + +def between( + expr: Union[_ColumnExpressionOrLiteralArgument[_T], _T], + lower_bound: Any, + upper_bound: Any, + symmetric: bool = False, +) -> BinaryExpression[bool]: + return sqlalchemy.between(expr, lower_bound, upper_bound, symmetric=symmetric) # type: ignore[arg-type] + + +def not_(clause: Union[_ColumnExpressionArgument[_T], _T]) -> ColumnElement[_T]: + return sqlalchemy.not_(clause) # type: ignore[arg-type] + + +def case( + *whens: Union[ + Tuple[Union[_ColumnExpressionArgument[bool], bool], Any], Mapping[Any, Any] + ], + value: Optional[Any] = None, + else_: Optional[Any] = None, +) -> Case[Any]: + return sqlalchemy.case(*whens, value=value, else_=else_) # type: ignore[arg-type] + + +def cast( + expression: Union[_ColumnExpressionOrLiteralArgument[Any], Any], + type_: "_TypeEngineArgument[_T]", +) -> Cast[_T]: + return sqlalchemy.cast(expression, type_) # type: ignore[arg-type] + + +def try_cast( + expression: Union[_ColumnExpressionOrLiteralArgument[Any], Any], + type_: "_TypeEngineArgument[_T]", +) -> TryCast[_T]: + return sqlalchemy.try_cast(expression, type_) # type: ignore[arg-type] + + +def desc( + column: Union[_ColumnExpressionOrStrLabelArgument[_T], _T], +) -> UnaryExpression[_T]: + return sqlalchemy.desc(column) # type: ignore[arg-type] + + +def distinct(expr: Union[_ColumnExpressionArgument[_T], _T]) -> UnaryExpression[_T]: + return sqlalchemy.distinct(expr) # type: ignore[arg-type] + + +def bitwise_not(expr: Union[_ColumnExpressionArgument[_T], _T]) -> UnaryExpression[_T]: + return sqlalchemy.bitwise_not(expr) # type: ignore[arg-type] + + +def extract(field: str, expr: Union[_ColumnExpressionArgument[Any], Any]) -> Extract: + return sqlalchemy.extract(field, expr) # type: ignore[arg-type] + + +def funcfilter( + func: FunctionElement[_T], *criterion: Union[_ColumnExpressionArgument[bool], bool] +) -> FunctionFilter[_T]: + return sqlalchemy.funcfilter(func, *criterion) # type: ignore[arg-type] + + +def label( + name: str, + element: Union[_ColumnExpressionArgument[_T], _T], + type_: Optional["_TypeEngineArgument[_T]"] = None, +) -> Label[_T]: + return sqlalchemy.label(name, element, type_=type_) # type: ignore[arg-type] + + +def nulls_first( + column: Union[_ColumnExpressionArgument[_T], _T] +) -> UnaryExpression[_T]: + return sqlalchemy.nulls_first(column) # type: ignore[arg-type] + + +def nulls_last(column: Union[_ColumnExpressionArgument[_T], _T]) -> UnaryExpression[_T]: + return sqlalchemy.nulls_last(column) # type: ignore[arg-type] + + +def or_( # type: ignore[empty-body] + initial_clause: Union[Literal[False], _ColumnExpressionArgument[bool], bool], + *clauses: Union[_ColumnExpressionArgument[bool], bool], +) -> ColumnElement[bool]: + return sqlalchemy.or_(initial_clause, *clauses) # type: ignore[arg-type] + + +def over( + element: FunctionElement[_T], + partition_by: Optional[ + Union[ + Iterable[Union[_ColumnExpressionArgument[Any], Any]], + _ColumnExpressionArgument[Any], + Any, + ] + ] = None, + order_by: Optional[ + Union[ + Iterable[Union[_ColumnExpressionArgument[Any], Any]], + _ColumnExpressionArgument[Any], + Any, + ] + ] = None, + range_: Optional[Tuple[Optional[int], Optional[int]]] = None, + rows: Optional[Tuple[Optional[int], Optional[int]]] = None, +) -> Over[_T]: + return sqlalchemy.over( + element, partition_by=partition_by, order_by=order_by, range_=range_, rows=rows + ) # type: ignore[arg-type] + + +def tuple_( + *clauses: Union[_ColumnExpressionArgument[Any], Any], + types: Optional[Sequence["_TypeEngineArgument[Any]"]] = None, +) -> Tuple[Any, ...]: + return sqlalchemy.tuple_(*clauses, types=types) # type: ignore[return-value] + + +def type_coerce( + expression: Union[_ColumnExpressionOrLiteralArgument[Any], Any], + type_: "_TypeEngineArgument[_T]", +) -> TypeCoerce[_T]: + return sqlalchemy.type_coerce(expression, type_) # type: ignore[arg-type] + + +def within_group( + element: FunctionElement[_T], *order_by: Union[_ColumnExpressionArgument[Any], Any] +) -> WithinGroup[_T]: + return sqlalchemy.within_group(element, *order_by) # type: ignore[arg-type] + + +# Separate this class in SelectBase, Select, and SelectOfScalar so that they can share +# where and having without having type overlap incompatibility in session.exec(). +class SelectBase(_Select[Tuple[_T]]): + inherit_cache = True + + def where(self, *whereclause: Union[_ColumnExpressionArgument[bool], bool]) -> Self: + """Return a new `Select` construct with the given expression added to + its `WHERE` clause, joined to the existing clause via `AND`, if any. + """ + return super().where(*whereclause) # type: ignore[arg-type] -_TSelect = TypeVar("_TSelect") + def having(self, *having: Union[_ColumnExpressionArgument[bool], bool]) -> Self: + """Return a new `Select` construct with the given expression added to + its `HAVING` clause, joined to the existing clause via `AND`, if any. + """ + return super().having(*having) # type: ignore[arg-type] -class Select(_Select, Generic[_TSelect]): +class Select(SelectBase[_T]): inherit_cache = True @@ -31,12 +239,15 @@ class Select(_Select, Generic[_TSelect]): # purpose. This is the same as a normal SQLAlchemy Select class where there's only one # entity, so the result will be converted to a scalar by default. This way writing # for loops on the results will feel natural. -class SelectOfScalar(_Select, Generic[_TSelect]): +class SelectOfScalar(SelectBase[_T]): inherit_cache = True -if TYPE_CHECKING: # pragma: no cover - from ..main import SQLModel +_TCCA = Union[ + TypedColumnsClauseRole[_T], + SQLCoreOperations[_T], + Type[_T], +] # Generated TypeVars start @@ -56,7 +267,7 @@ class SelectOfScalar(_Select, Generic[_TSelect]): None, ) -_TModel_0 = TypeVar("_TModel_0", bound="SQLModel") +_T0 = TypeVar("_T0") _TScalar_1 = TypeVar( @@ -74,7 +285,7 @@ class SelectOfScalar(_Select, Generic[_TSelect]): None, ) -_TModel_1 = TypeVar("_TModel_1", bound="SQLModel") +_T1 = TypeVar("_T1") _TScalar_2 = TypeVar( @@ -92,7 +303,7 @@ class SelectOfScalar(_Select, Generic[_TSelect]): None, ) -_TModel_2 = TypeVar("_TModel_2", bound="SQLModel") +_T2 = TypeVar("_T2") _TScalar_3 = TypeVar( @@ -110,19 +321,19 @@ class SelectOfScalar(_Select, Generic[_TSelect]): None, ) -_TModel_3 = TypeVar("_TModel_3", bound="SQLModel") +_T3 = TypeVar("_T3") # Generated TypeVars end @overload -def select(entity_0: _TScalar_0, **kw: Any) -> SelectOfScalar[_TScalar_0]: # type: ignore +def select(__ent0: _TScalar_0) -> SelectOfScalar[_TScalar_0]: # type: ignore ... @overload -def select(entity_0: Type[_TModel_0], **kw: Any) -> SelectOfScalar[_TModel_0]: # type: ignore +def select(__ent0: _TCCA[_T0]) -> SelectOfScalar[_T0]: ... @@ -133,7 +344,6 @@ def select(entity_0: Type[_TModel_0], **kw: Any) -> SelectOfScalar[_TModel_0]: def select( # type: ignore entity_0: _TScalar_0, entity_1: _TScalar_1, - **kw: Any, ) -> Select[Tuple[_TScalar_0, _TScalar_1]]: ... @@ -141,27 +351,24 @@ def select( # type: ignore @overload def select( # type: ignore entity_0: _TScalar_0, - entity_1: Type[_TModel_1], - **kw: Any, -) -> Select[Tuple[_TScalar_0, _TModel_1]]: + __ent1: _TCCA[_T1], +) -> Select[Tuple[_TScalar_0, _T1]]: ... @overload def select( # type: ignore - entity_0: Type[_TModel_0], + __ent0: _TCCA[_T0], entity_1: _TScalar_1, - **kw: Any, -) -> Select[Tuple[_TModel_0, _TScalar_1]]: +) -> Select[Tuple[_T0, _TScalar_1]]: ... @overload def select( # type: ignore - entity_0: Type[_TModel_0], - entity_1: Type[_TModel_1], - **kw: Any, -) -> Select[Tuple[_TModel_0, _TModel_1]]: + __ent0: _TCCA[_T0], + __ent1: _TCCA[_T1], +) -> Select[Tuple[_T0, _T1]]: ... @@ -170,7 +377,6 @@ def select( # type: ignore entity_0: _TScalar_0, entity_1: _TScalar_1, entity_2: _TScalar_2, - **kw: Any, ) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2]]: ... @@ -179,69 +385,62 @@ def select( # type: ignore def select( # type: ignore entity_0: _TScalar_0, entity_1: _TScalar_1, - entity_2: Type[_TModel_2], - **kw: Any, -) -> Select[Tuple[_TScalar_0, _TScalar_1, _TModel_2]]: + __ent2: _TCCA[_T2], +) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2]]: ... @overload def select( # type: ignore entity_0: _TScalar_0, - entity_1: Type[_TModel_1], + __ent1: _TCCA[_T1], entity_2: _TScalar_2, - **kw: Any, -) -> Select[Tuple[_TScalar_0, _TModel_1, _TScalar_2]]: +) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2]]: ... @overload def select( # type: ignore entity_0: _TScalar_0, - entity_1: Type[_TModel_1], - entity_2: Type[_TModel_2], - **kw: Any, -) -> Select[Tuple[_TScalar_0, _TModel_1, _TModel_2]]: + __ent1: _TCCA[_T1], + __ent2: _TCCA[_T2], +) -> Select[Tuple[_TScalar_0, _T1, _T2]]: ... @overload def select( # type: ignore - entity_0: Type[_TModel_0], + __ent0: _TCCA[_T0], entity_1: _TScalar_1, entity_2: _TScalar_2, - **kw: Any, -) -> Select[Tuple[_TModel_0, _TScalar_1, _TScalar_2]]: +) -> Select[Tuple[_T0, _TScalar_1, _TScalar_2]]: ... @overload def select( # type: ignore - entity_0: Type[_TModel_0], + __ent0: _TCCA[_T0], entity_1: _TScalar_1, - entity_2: Type[_TModel_2], - **kw: Any, -) -> Select[Tuple[_TModel_0, _TScalar_1, _TModel_2]]: + __ent2: _TCCA[_T2], +) -> Select[Tuple[_T0, _TScalar_1, _T2]]: ... @overload def select( # type: ignore - entity_0: Type[_TModel_0], - entity_1: Type[_TModel_1], + __ent0: _TCCA[_T0], + __ent1: _TCCA[_T1], entity_2: _TScalar_2, - **kw: Any, -) -> Select[Tuple[_TModel_0, _TModel_1, _TScalar_2]]: +) -> Select[Tuple[_T0, _T1, _TScalar_2]]: ... @overload def select( # type: ignore - entity_0: Type[_TModel_0], - entity_1: Type[_TModel_1], - entity_2: Type[_TModel_2], - **kw: Any, -) -> Select[Tuple[_TModel_0, _TModel_1, _TModel_2]]: + __ent0: _TCCA[_T0], + __ent1: _TCCA[_T1], + __ent2: _TCCA[_T2], +) -> Select[Tuple[_T0, _T1, _T2]]: ... @@ -251,7 +450,6 @@ def select( # type: ignore entity_1: _TScalar_1, entity_2: _TScalar_2, entity_3: _TScalar_3, - **kw: Any, ) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2, _TScalar_3]]: ... @@ -261,9 +459,8 @@ def select( # type: ignore entity_0: _TScalar_0, entity_1: _TScalar_1, entity_2: _TScalar_2, - entity_3: Type[_TModel_3], - **kw: Any, -) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2, _TModel_3]]: + __ent3: _TCCA[_T3], +) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2, _T3]]: ... @@ -271,10 +468,9 @@ def select( # type: ignore def select( # type: ignore entity_0: _TScalar_0, entity_1: _TScalar_1, - entity_2: Type[_TModel_2], + __ent2: _TCCA[_T2], entity_3: _TScalar_3, - **kw: Any, -) -> Select[Tuple[_TScalar_0, _TScalar_1, _TModel_2, _TScalar_3]]: +) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2, _TScalar_3]]: ... @@ -282,156 +478,142 @@ def select( # type: ignore def select( # type: ignore entity_0: _TScalar_0, entity_1: _TScalar_1, - entity_2: Type[_TModel_2], - entity_3: Type[_TModel_3], - **kw: Any, -) -> Select[Tuple[_TScalar_0, _TScalar_1, _TModel_2, _TModel_3]]: + __ent2: _TCCA[_T2], + __ent3: _TCCA[_T3], +) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2, _T3]]: ... @overload def select( # type: ignore entity_0: _TScalar_0, - entity_1: Type[_TModel_1], + __ent1: _TCCA[_T1], entity_2: _TScalar_2, entity_3: _TScalar_3, - **kw: Any, -) -> Select[Tuple[_TScalar_0, _TModel_1, _TScalar_2, _TScalar_3]]: +) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2, _TScalar_3]]: ... @overload def select( # type: ignore entity_0: _TScalar_0, - entity_1: Type[_TModel_1], + __ent1: _TCCA[_T1], entity_2: _TScalar_2, - entity_3: Type[_TModel_3], - **kw: Any, -) -> Select[Tuple[_TScalar_0, _TModel_1, _TScalar_2, _TModel_3]]: + __ent3: _TCCA[_T3], +) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2, _T3]]: ... @overload def select( # type: ignore entity_0: _TScalar_0, - entity_1: Type[_TModel_1], - entity_2: Type[_TModel_2], + __ent1: _TCCA[_T1], + __ent2: _TCCA[_T2], entity_3: _TScalar_3, - **kw: Any, -) -> Select[Tuple[_TScalar_0, _TModel_1, _TModel_2, _TScalar_3]]: +) -> Select[Tuple[_TScalar_0, _T1, _T2, _TScalar_3]]: ... @overload def select( # type: ignore entity_0: _TScalar_0, - entity_1: Type[_TModel_1], - entity_2: Type[_TModel_2], - entity_3: Type[_TModel_3], - **kw: Any, -) -> Select[Tuple[_TScalar_0, _TModel_1, _TModel_2, _TModel_3]]: + __ent1: _TCCA[_T1], + __ent2: _TCCA[_T2], + __ent3: _TCCA[_T3], +) -> Select[Tuple[_TScalar_0, _T1, _T2, _T3]]: ... @overload def select( # type: ignore - entity_0: Type[_TModel_0], + __ent0: _TCCA[_T0], entity_1: _TScalar_1, entity_2: _TScalar_2, entity_3: _TScalar_3, - **kw: Any, -) -> Select[Tuple[_TModel_0, _TScalar_1, _TScalar_2, _TScalar_3]]: +) -> Select[Tuple[_T0, _TScalar_1, _TScalar_2, _TScalar_3]]: ... @overload def select( # type: ignore - entity_0: Type[_TModel_0], + __ent0: _TCCA[_T0], entity_1: _TScalar_1, entity_2: _TScalar_2, - entity_3: Type[_TModel_3], - **kw: Any, -) -> Select[Tuple[_TModel_0, _TScalar_1, _TScalar_2, _TModel_3]]: + __ent3: _TCCA[_T3], +) -> Select[Tuple[_T0, _TScalar_1, _TScalar_2, _T3]]: ... @overload def select( # type: ignore - entity_0: Type[_TModel_0], + __ent0: _TCCA[_T0], entity_1: _TScalar_1, - entity_2: Type[_TModel_2], + __ent2: _TCCA[_T2], entity_3: _TScalar_3, - **kw: Any, -) -> Select[Tuple[_TModel_0, _TScalar_1, _TModel_2, _TScalar_3]]: +) -> Select[Tuple[_T0, _TScalar_1, _T2, _TScalar_3]]: ... @overload def select( # type: ignore - entity_0: Type[_TModel_0], + __ent0: _TCCA[_T0], entity_1: _TScalar_1, - entity_2: Type[_TModel_2], - entity_3: Type[_TModel_3], - **kw: Any, -) -> Select[Tuple[_TModel_0, _TScalar_1, _TModel_2, _TModel_3]]: + __ent2: _TCCA[_T2], + __ent3: _TCCA[_T3], +) -> Select[Tuple[_T0, _TScalar_1, _T2, _T3]]: ... @overload def select( # type: ignore - entity_0: Type[_TModel_0], - entity_1: Type[_TModel_1], + __ent0: _TCCA[_T0], + __ent1: _TCCA[_T1], entity_2: _TScalar_2, entity_3: _TScalar_3, - **kw: Any, -) -> Select[Tuple[_TModel_0, _TModel_1, _TScalar_2, _TScalar_3]]: +) -> Select[Tuple[_T0, _T1, _TScalar_2, _TScalar_3]]: ... @overload def select( # type: ignore - entity_0: Type[_TModel_0], - entity_1: Type[_TModel_1], + __ent0: _TCCA[_T0], + __ent1: _TCCA[_T1], entity_2: _TScalar_2, - entity_3: Type[_TModel_3], - **kw: Any, -) -> Select[Tuple[_TModel_0, _TModel_1, _TScalar_2, _TModel_3]]: + __ent3: _TCCA[_T3], +) -> Select[Tuple[_T0, _T1, _TScalar_2, _T3]]: ... @overload def select( # type: ignore - entity_0: Type[_TModel_0], - entity_1: Type[_TModel_1], - entity_2: Type[_TModel_2], + __ent0: _TCCA[_T0], + __ent1: _TCCA[_T1], + __ent2: _TCCA[_T2], entity_3: _TScalar_3, - **kw: Any, -) -> Select[Tuple[_TModel_0, _TModel_1, _TModel_2, _TScalar_3]]: +) -> Select[Tuple[_T0, _T1, _T2, _TScalar_3]]: ... @overload def select( # type: ignore - entity_0: Type[_TModel_0], - entity_1: Type[_TModel_1], - entity_2: Type[_TModel_2], - entity_3: Type[_TModel_3], - **kw: Any, -) -> Select[Tuple[_TModel_0, _TModel_1, _TModel_2, _TModel_3]]: + __ent0: _TCCA[_T0], + __ent1: _TCCA[_T1], + __ent2: _TCCA[_T2], + __ent3: _TCCA[_T3], +) -> Select[Tuple[_T0, _T1, _T2, _T3]]: ... # Generated overloads end -def select(*entities: Any, **kw: Any) -> Union[Select, SelectOfScalar]: # type: ignore +def select(*entities: Any) -> Union[Select, SelectOfScalar]: # type: ignore if len(entities) == 1: - return SelectOfScalar._create(*entities, **kw) # type: ignore - return Select._create(*entities, **kw) # type: ignore + return SelectOfScalar(*entities) + return Select(*entities) -# TODO: add several @overload from Python types to SQLAlchemy equivalents -def col(column_expression: Any) -> ColumnClause: # type: ignore +def col(column_expression: _T) -> Mapped[_T]: if not isinstance(column_expression, (ColumnClause, Column, InstrumentedAttribute)): raise RuntimeError(f"Not a SQLAlchemy column: {column_expression}") - return column_expression + return column_expression # type: ignore diff --git a/sqlmodel/sql/expression.py.jinja2 b/sqlmodel/sql/expression.py.jinja2 index 26d12a0395..f1a25419c0 100644 --- a/sqlmodel/sql/expression.py.jinja2 +++ b/sqlmodel/sql/expression.py.jinja2 @@ -1,9 +1,9 @@ from datetime import datetime from typing import ( - TYPE_CHECKING, Any, - Generic, + Iterable, Mapping, + Optional, Sequence, Tuple, Type, @@ -13,28 +13,243 @@ from typing import ( ) from uuid import UUID -from sqlalchemy import Column -from sqlalchemy.orm import InstrumentedAttribute -from sqlalchemy.sql.elements import ColumnClause +import sqlalchemy +from sqlalchemy import ( + Column, + ColumnElement, + Extract, + FunctionElement, + FunctionFilter, + Label, + Over, + TypeCoerce, + WithinGroup, +) +from sqlalchemy.orm import InstrumentedAttribute, Mapped +from sqlalchemy.sql._typing import ( + _ColumnExpressionArgument, + _ColumnExpressionOrLiteralArgument, + _ColumnExpressionOrStrLabelArgument, +) +from sqlalchemy.sql.elements import ( + BinaryExpression, + Case, + Cast, + CollectionAggregate, + ColumnClause, + SQLCoreOperations, + TryCast, + UnaryExpression, +) from sqlalchemy.sql.expression import Select as _Select +from sqlalchemy.sql.roles import TypedColumnsClauseRole +from sqlalchemy.sql.type_api import TypeEngine +from typing_extensions import Literal, Self + +_T = TypeVar("_T") + +_TypeEngineArgument = Union[Type[TypeEngine[_T]], TypeEngine[_T]] + +# Redefine operatos that would only take a column expresion to also take the (virtual) +# types of Pydantic models, e.g. str instead of only Mapped[str]. + + +def all_(expr: Union[_ColumnExpressionArgument[_T], _T]) -> CollectionAggregate[bool]: + return sqlalchemy.all_(expr) # type: ignore[arg-type] + + +def and_( + initial_clause: Union[Literal[True], _ColumnExpressionArgument[bool], bool], + *clauses: Union[_ColumnExpressionArgument[bool], bool], +) -> ColumnElement[bool]: + return sqlalchemy.and_(initial_clause, *clauses) # type: ignore[arg-type] + + +def any_(expr: Union[_ColumnExpressionArgument[_T], _T]) -> CollectionAggregate[bool]: + return sqlalchemy.any_(expr) # type: ignore[arg-type] + + +def asc( + column: Union[_ColumnExpressionOrStrLabelArgument[_T], _T], +) -> UnaryExpression[_T]: + return sqlalchemy.asc(column) # type: ignore[arg-type] + + +def collate( + expression: Union[_ColumnExpressionArgument[str], str], collation: str +) -> BinaryExpression[str]: + return sqlalchemy.collate(expression, collation) # type: ignore[arg-type] + + +def between( + expr: Union[_ColumnExpressionOrLiteralArgument[_T], _T], + lower_bound: Any, + upper_bound: Any, + symmetric: bool = False, +) -> BinaryExpression[bool]: + return sqlalchemy.between(expr, lower_bound, upper_bound, symmetric=symmetric) # type: ignore[arg-type] + + +def not_(clause: Union[_ColumnExpressionArgument[_T], _T]) -> ColumnElement[_T]: + return sqlalchemy.not_(clause) # type: ignore[arg-type] + + +def case( + *whens: Union[ + Tuple[Union[_ColumnExpressionArgument[bool], bool], Any], Mapping[Any, Any] + ], + value: Optional[Any] = None, + else_: Optional[Any] = None, +) -> Case[Any]: + return sqlalchemy.case(*whens, value=value, else_=else_) # type: ignore[arg-type] + + +def cast( + expression: Union[_ColumnExpressionOrLiteralArgument[Any], Any], + type_: "_TypeEngineArgument[_T]", +) -> Cast[_T]: + return sqlalchemy.cast(expression, type_) # type: ignore[arg-type] + + +def try_cast( + expression: Union[_ColumnExpressionOrLiteralArgument[Any], Any], + type_: "_TypeEngineArgument[_T]", +) -> TryCast[_T]: + return sqlalchemy.try_cast(expression, type_) # type: ignore[arg-type] + + +def desc( + column: Union[_ColumnExpressionOrStrLabelArgument[_T], _T], +) -> UnaryExpression[_T]: + return sqlalchemy.desc(column) # type: ignore[arg-type] + + +def distinct(expr: Union[_ColumnExpressionArgument[_T], _T]) -> UnaryExpression[_T]: + return sqlalchemy.distinct(expr) # type: ignore[arg-type] + -_TSelect = TypeVar("_TSelect") +def bitwise_not(expr: Union[_ColumnExpressionArgument[_T], _T]) -> UnaryExpression[_T]: + return sqlalchemy.bitwise_not(expr) # type: ignore[arg-type] -class Select(_Select, Generic[_TSelect]): + +def extract(field: str, expr: Union[_ColumnExpressionArgument[Any], Any]) -> Extract: + return sqlalchemy.extract(field, expr) # type: ignore[arg-type] + + +def funcfilter( + func: FunctionElement[_T], *criterion: Union[_ColumnExpressionArgument[bool], bool] +) -> FunctionFilter[_T]: + return sqlalchemy.funcfilter(func, *criterion) # type: ignore[arg-type] + + +def label( + name: str, + element: Union[_ColumnExpressionArgument[_T], _T], + type_: Optional["_TypeEngineArgument[_T]"] = None, +) -> Label[_T]: + return sqlalchemy.label(name, element, type_=type_) # type: ignore[arg-type] + + +def nulls_first( + column: Union[_ColumnExpressionArgument[_T], _T] +) -> UnaryExpression[_T]: + return sqlalchemy.nulls_first(column) # type: ignore[arg-type] + + +def nulls_last(column: Union[_ColumnExpressionArgument[_T], _T]) -> UnaryExpression[_T]: + return sqlalchemy.nulls_last(column) # type: ignore[arg-type] + + +def or_( # type: ignore[empty-body] + initial_clause: Union[Literal[False], _ColumnExpressionArgument[bool], bool], + *clauses: Union[_ColumnExpressionArgument[bool], bool], +) -> ColumnElement[bool]: + return sqlalchemy.or_(initial_clause, *clauses) # type: ignore[arg-type] + + +def over( + element: FunctionElement[_T], + partition_by: Optional[ + Union[ + Iterable[Union[_ColumnExpressionArgument[Any], Any]], + _ColumnExpressionArgument[Any], + Any, + ] + ] = None, + order_by: Optional[ + Union[ + Iterable[Union[_ColumnExpressionArgument[Any], Any]], + _ColumnExpressionArgument[Any], + Any, + ] + ] = None, + range_: Optional[Tuple[Optional[int], Optional[int]]] = None, + rows: Optional[Tuple[Optional[int], Optional[int]]] = None, +) -> Over[_T]: + return sqlalchemy.over( + element, partition_by=partition_by, order_by=order_by, range_=range_, rows=rows + ) # type: ignore[arg-type] + + +def tuple_( + *clauses: Union[_ColumnExpressionArgument[Any], Any], + types: Optional[Sequence["_TypeEngineArgument[Any]"]] = None, +) -> Tuple[Any, ...]: + return sqlalchemy.tuple_(*clauses, types=types) # type: ignore[return-value] + + +def type_coerce( + expression: Union[_ColumnExpressionOrLiteralArgument[Any], Any], + type_: "_TypeEngineArgument[_T]", +) -> TypeCoerce[_T]: + return sqlalchemy.type_coerce(expression, type_) # type: ignore[arg-type] + + +def within_group( + element: FunctionElement[_T], *order_by: Union[_ColumnExpressionArgument[Any], Any] +) -> WithinGroup[_T]: + return sqlalchemy.within_group(element, *order_by) # type: ignore[arg-type] + + +# Separate this class in SelectBase, Select, and SelectOfScalar so that they can share +# where and having without having type overlap incompatibility in session.exec(). +class SelectBase(_Select[Tuple[_T]]): inherit_cache = True + def where(self, *whereclause: Union[_ColumnExpressionArgument[bool], bool]) -> Self: + """Return a new `Select` construct with the given expression added to + its `WHERE` clause, joined to the existing clause via `AND`, if any. + """ + return super().where(*whereclause) # type: ignore[arg-type] + + def having(self, *having: Union[_ColumnExpressionArgument[bool], bool]) -> Self: + """Return a new `Select` construct with the given expression added to + its `HAVING` clause, joined to the existing clause via `AND`, if any. + """ + return super().having(*having) # type: ignore[arg-type] + + +class Select(SelectBase[_T]): + inherit_cache = True + + # This is not comparable to sqlalchemy.sql.selectable.ScalarSelect, that has a different # purpose. This is the same as a normal SQLAlchemy Select class where there's only one # entity, so the result will be converted to a scalar by default. This way writing # for loops on the results will feel natural. -class SelectOfScalar(_Select, Generic[_TSelect]): +class SelectOfScalar(SelectBase[_T]): inherit_cache = True -if TYPE_CHECKING: # pragma: no cover - from ..main import SQLModel + +_TCCA = Union[ + TypedColumnsClauseRole[_T], + SQLCoreOperations[_T], + Type[_T], +] # Generated TypeVars start + {% for i in range(number_of_types) %} _TScalar_{{ i }} = TypeVar( "_TScalar_{{ i }}", @@ -51,19 +266,19 @@ _TScalar_{{ i }} = TypeVar( None, ) -_TModel_{{ i }} = TypeVar("_TModel_{{ i }}", bound="SQLModel") +_T{{ i }} = TypeVar("_T{{ i }}") {% endfor %} # Generated TypeVars end @overload -def select(entity_0: _TScalar_0, **kw: Any) -> SelectOfScalar[_TScalar_0]: # type: ignore +def select(__ent0: _TScalar_0) -> SelectOfScalar[_TScalar_0]: # type: ignore ... @overload -def select(entity_0: Type[_TModel_0], **kw: Any) -> SelectOfScalar[_TModel_0]: # type: ignore +def select(__ent0: _TCCA[_T0]) -> SelectOfScalar[_T0]: ... @@ -73,7 +288,7 @@ def select(entity_0: Type[_TModel_0], **kw: Any) -> SelectOfScalar[_TModel_0]: @overload def select( # type: ignore - {% for arg in signature[0] %}{{ arg.name }}: {{ arg.annotation }}, {% endfor %}**kw: Any, + {% for arg in signature[0] %}{{ arg.name }}: {{ arg.annotation }}, {% endfor %} ) -> Select[Tuple[{%for ret in signature[1] %}{{ ret }} {% if not loop.last %}, {% endif %}{% endfor %}]]: ... @@ -81,14 +296,14 @@ def select( # type: ignore # Generated overloads end -def select(*entities: Any, **kw: Any) -> Union[Select, SelectOfScalar]: # type: ignore + +def select(*entities: Any) -> Union[Select, SelectOfScalar]: # type: ignore if len(entities) == 1: - return SelectOfScalar._create(*entities, **kw) # type: ignore - return Select._create(*entities, **kw) # type: ignore + return SelectOfScalar(*entities) + return Select(*entities) -# TODO: add several @overload from Python types to SQLAlchemy equivalents -def col(column_expression: Any) -> ColumnClause: # type: ignore +def col(column_expression: _T) -> Mapped[_T]: if not isinstance(column_expression, (ColumnClause, Column, InstrumentedAttribute)): raise RuntimeError(f"Not a SQLAlchemy column: {column_expression}") - return column_expression + return column_expression # type: ignore diff --git a/sqlmodel/sql/sqltypes.py b/sqlmodel/sql/sqltypes.py index 17d9b06126..5a4bb04ef1 100644 --- a/sqlmodel/sql/sqltypes.py +++ b/sqlmodel/sql/sqltypes.py @@ -15,7 +15,7 @@ class AutoString(types.TypeDecorator): # type: ignore def load_dialect_impl(self, dialect: Dialect) -> "types.TypeEngine[Any]": impl = cast(types.String, self.impl) if impl.length is None and dialect.name == "mysql": - return dialect.type_descriptor(types.String(self.mysql_default_length)) # type: ignore + return dialect.type_descriptor(types.String(self.mysql_default_length)) return super().load_dialect_impl(dialect) @@ -32,11 +32,11 @@ class GUID(types.TypeDecorator): # type: ignore impl = CHAR cache_ok = True - def load_dialect_impl(self, dialect: Dialect) -> TypeEngine: # type: ignore + def load_dialect_impl(self, dialect: Dialect) -> TypeEngine[Any]: if dialect.name == "postgresql": - return dialect.type_descriptor(UUID()) # type: ignore + return dialect.type_descriptor(UUID()) else: - return dialect.type_descriptor(CHAR(32)) # type: ignore + return dialect.type_descriptor(CHAR(32)) def process_bind_param(self, value: Any, dialect: Dialect) -> Optional[str]: if value is None: diff --git a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py index b08affb920..6a55d6cb98 100644 --- a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py @@ -59,7 +59,7 @@ def test_tutorial(clear_sqlmodel): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/heroes/": { @@ -315,7 +315,9 @@ def test_tutorial(clear_sqlmodel): "loc": { "title": "Location", "type": "array", - "items": {"type": "string"}, + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, diff --git a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py index 0aee3ca004..2709231504 100644 --- a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py @@ -64,7 +64,7 @@ def test_tutorial(clear_sqlmodel): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/heroes/": { @@ -239,7 +239,9 @@ def test_tutorial(clear_sqlmodel): "loc": { "title": "Location", "type": "array", - "items": {"type": "string"}, + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py index 8d99cf9f5b..dc5a3cb8ff 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py @@ -5,7 +5,7 @@ from sqlmodel.pool import StaticPool openapi_schema = { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/heroes/": { @@ -103,7 +103,7 @@ "loc": { "title": "Location", "type": "array", - "items": {"type": "string"}, + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py index 94a41b3076..e3c20404c0 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py @@ -5,7 +5,7 @@ from sqlmodel.pool import StaticPool openapi_schema = { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/heroes/": { @@ -103,7 +103,7 @@ "loc": { "title": "Location", "type": "array", - "items": {"type": "string"}, + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, diff --git a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py index 0609ae41ff..0a599574d5 100644 --- a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py @@ -3,7 +3,7 @@ from sqlmodel.pool import StaticPool openapi_schema = { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/heroes/": { @@ -135,7 +135,7 @@ "loc": { "title": "Location", "type": "array", - "items": {"type": "string"}, + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, diff --git a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py index 8869862e95..fb08b9a5fd 100644 --- a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py @@ -107,7 +107,7 @@ def test_tutorial(clear_sqlmodel): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/heroes/": { @@ -622,7 +622,9 @@ def test_tutorial(clear_sqlmodel): "loc": { "title": "Location", "type": "array", - "items": {"type": "string"}, + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, diff --git a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py index ebb3046ef3..968fefa8ca 100644 --- a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py @@ -3,7 +3,7 @@ from sqlmodel.pool import StaticPool openapi_schema = { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/heroes/": { @@ -91,7 +91,7 @@ "loc": { "title": "Location", "type": "array", - "items": {"type": "string"}, + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, diff --git a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py index cb0a6f9282..6f97cbf92b 100644 --- a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py @@ -59,7 +59,7 @@ def test_tutorial(clear_sqlmodel): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/heroes/": { @@ -315,7 +315,9 @@ def test_tutorial(clear_sqlmodel): "loc": { "title": "Location", "type": "array", - "items": {"type": "string"}, + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, diff --git a/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py index eb834ec2a4..435155d6e9 100644 --- a/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py @@ -3,7 +3,7 @@ from sqlmodel.pool import StaticPool openapi_schema = { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/heroes/": { @@ -79,7 +79,7 @@ "loc": { "title": "Location", "type": "array", - "items": {"type": "string"}, + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, diff --git a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py index e66c975142..42f87cef76 100644 --- a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py @@ -94,7 +94,7 @@ def test_tutorial(clear_sqlmodel): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/heroes/": { @@ -579,7 +579,9 @@ def test_tutorial(clear_sqlmodel): "loc": { "title": "Location", "type": "array", - "items": {"type": "string"}, + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py index 49906256c9..a4573ef11b 100644 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py @@ -66,7 +66,7 @@ def test_tutorial(clear_sqlmodel): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/heroes/": { @@ -294,7 +294,9 @@ def test_tutorial(clear_sqlmodel): "loc": { "title": "Location", "type": "array", - "items": {"type": "string"}, + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, From 6d00f6fcbdf887d99abf4445481c65d9797b7b25 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 18 Nov 2023 11:30:55 +0000 Subject: [PATCH 294/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 819f97244e..4278a65b07 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Features + +* ✨ Upgrade SQLAlchemy to 2.0, including initial work by farahats9. PR [#700](https://github.com/tiangolo/sqlmodel/pull/700) by [@tiangolo](https://github.com/tiangolo). + ### Internal * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#686](https://github.com/tiangolo/sqlmodel/pull/686) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From 382b1b0cbbff83275f3315d1d8780b18b50885cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 18 Nov 2023 12:32:24 +0100 Subject: [PATCH 295/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 4278a65b07..f94771c7df 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,7 +4,7 @@ ### Features -* ✨ Upgrade SQLAlchemy to 2.0, including initial work by farahats9. PR [#700](https://github.com/tiangolo/sqlmodel/pull/700) by [@tiangolo](https://github.com/tiangolo). +* ✨ Upgrade SQLAlchemy to 2.0. PR [#700](https://github.com/tiangolo/sqlmodel/pull/700) by [@tiangolo](https://github.com/tiangolo) including initial work in PR [#563](https://github.com/tiangolo/sqlmodel/pull/563) by [@farahats9](https://github.com/farahats9). ### Internal From b1c2f822c9db036afe1b7ec81d335368e9546756 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 18 Nov 2023 12:32:59 +0100 Subject: [PATCH 296/906] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.0.?= =?UTF-8?q?12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 2 ++ sqlmodel/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index f94771c7df..e3c35629d8 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest Changes +## 0.0.12 + ### Features * ✨ Upgrade SQLAlchemy to 2.0. PR [#700](https://github.com/tiangolo/sqlmodel/pull/700) by [@tiangolo](https://github.com/tiangolo) including initial work in PR [#563](https://github.com/tiangolo/sqlmodel/pull/563) by [@farahats9](https://github.com/farahats9). diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index e943257165..5b117a1c05 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.0.11" +__version__ = "0.0.12" # Re-export from SQLAlchemy from sqlalchemy.engine import create_engine as create_engine From a974d9104f11863cb46452bc02e1d10d4fe5acba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 26 Nov 2023 14:57:12 +0100 Subject: [PATCH 297/906] =?UTF-8?q?=E2=9C=85=20Move=20OpenAPI=20tests=20in?= =?UTF-8?q?line=20to=20simplify=20updating=20them=20with=20Pydantic=20v2?= =?UTF-8?q?=20(#709)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_multiple_models/test_tutorial001.py | 228 +++++++------- .../test_multiple_models/test_tutorial002.py | 228 +++++++------- .../test_read_one/test_tutorial001.py | 298 +++++++++--------- .../test_response_model/test_tutorial001.py | 204 ++++++------ .../test_simple_hero_api/test_tutorial001.py | 178 +++++------ 5 files changed, 584 insertions(+), 552 deletions(-) diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py index dc5a3cb8ff..7444f8858d 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py @@ -4,115 +4,6 @@ from sqlmodel import create_engine from sqlmodel.pool import StaticPool -openapi_schema = { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": {"$ref": "#/components/schemas/HeroRead"}, - } - } - }, - } - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroCreate"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroRead"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - }, - }, - "HeroRead": { - "title": "HeroRead", - "required": ["id", "name", "secret_name"], - "type": "object", - "properties": { - "id": {"title": "Id", "type": "integer"}, - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - def test_tutorial(clear_sqlmodel): from docs_src.tutorial.fastapi.multiple_models import tutorial001 as mod @@ -166,7 +57,124 @@ def test_tutorial(clear_sqlmodel): assert response.status_code == 200, response.text - assert data == openapi_schema + assert data == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Heroes Heroes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/HeroRead" + }, + } + } + }, + } + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "HeroCreate": { + "title": "HeroCreate", + "required": ["name", "secret_name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "HeroRead": { + "title": "HeroRead", + "required": ["id", "name", "secret_name"], + "type": "object", + "properties": { + "id": {"title": "Id", "type": "integer"}, + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } # Test inherited indexes insp: Inspector = inspect(mod.engine) diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py index e3c20404c0..4a6bb7499e 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py @@ -4,115 +4,6 @@ from sqlmodel import create_engine from sqlmodel.pool import StaticPool -openapi_schema = { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": {"$ref": "#/components/schemas/HeroRead"}, - } - } - }, - } - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroCreate"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroRead"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - }, - }, - "HeroRead": { - "title": "HeroRead", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "id": {"title": "Id", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - def test_tutorial(clear_sqlmodel): from docs_src.tutorial.fastapi.multiple_models import tutorial002 as mod @@ -166,7 +57,124 @@ def test_tutorial(clear_sqlmodel): assert response.status_code == 200, response.text - assert data == openapi_schema + assert data == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Heroes Heroes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/HeroRead" + }, + } + } + }, + } + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "HeroCreate": { + "title": "HeroCreate", + "required": ["name", "secret_name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "HeroRead": { + "title": "HeroRead", + "required": ["name", "secret_name", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "id": {"title": "Id", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } # Test inherited indexes insp: Inspector = inspect(mod.engine) diff --git a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py index 0a599574d5..5d2327095e 100644 --- a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py @@ -2,149 +2,6 @@ from sqlmodel import create_engine from sqlmodel.pool import StaticPool -openapi_schema = { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": {"$ref": "#/components/schemas/HeroRead"}, - } - } - }, - } - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroCreate"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroRead"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/HeroRead"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - }, - }, - "HeroRead": { - "title": "HeroRead", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "id": {"title": "Id", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - def test_tutorial(clear_sqlmodel): from docs_src.tutorial.fastapi.read_one import tutorial001 as mod @@ -185,4 +42,157 @@ def test_tutorial(clear_sqlmodel): assert response.status_code == 200, response.text - assert data == openapi_schema + assert data == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Heroes Heroes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/HeroRead" + }, + } + } + }, + } + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/heroes/{hero_id}": { + "get": { + "summary": "Read Hero", + "operationId": "read_hero_heroes__hero_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "HeroCreate": { + "title": "HeroCreate", + "required": ["name", "secret_name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "HeroRead": { + "title": "HeroRead", + "required": ["name", "secret_name", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "id": {"title": "Id", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py index 968fefa8ca..ca8a41845e 100644 --- a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py @@ -2,105 +2,6 @@ from sqlmodel import create_engine from sqlmodel.pool import StaticPool -openapi_schema = { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": {"$ref": "#/components/schemas/Hero"}, - } - } - }, - } - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Hero"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Hero"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Hero": { - "title": "Hero", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "id": {"title": "Id", "type": "integer"}, - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - def test_tutorial(clear_sqlmodel): from docs_src.tutorial.fastapi.response_model import tutorial001 as mod @@ -134,4 +35,107 @@ def test_tutorial(clear_sqlmodel): assert response.status_code == 200, response.text - assert data == openapi_schema + assert data == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Heroes Heroes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/Hero" + }, + } + } + }, + } + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Hero"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Hero"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "Hero": { + "title": "Hero", + "required": ["name", "secret_name"], + "type": "object", + "properties": { + "id": {"title": "Id", "type": "integer"}, + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py index 435155d6e9..2136ed8a1f 100644 --- a/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py @@ -2,93 +2,6 @@ from sqlmodel import create_engine from sqlmodel.pool import StaticPool -openapi_schema = { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Hero"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Hero": { - "title": "Hero", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "id": {"title": "Id", "type": "integer"}, - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - def test_tutorial(clear_sqlmodel): from docs_src.tutorial.fastapi.simple_hero_api import tutorial001 as mod @@ -142,4 +55,93 @@ def test_tutorial(clear_sqlmodel): assert response.status_code == 200, response.text - assert data == openapi_schema + assert data == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Hero"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "Hero": { + "title": "Hero", + "required": ["name", "secret_name"], + "type": "object", + "properties": { + "id": {"title": "Id", "type": "integer"}, + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } From 781a2d6b0afa1290ce20708019c5a2de08c049ca Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 26 Nov 2023 13:57:29 +0000 Subject: [PATCH 298/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index e3c35629d8..c496468f49 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Refactors + +* ✅ Move OpenAPI tests inline to simplify updating them with Pydantic v2. PR [#709](https://github.com/tiangolo/sqlmodel/pull/709) by [@tiangolo](https://github.com/tiangolo). + ## 0.0.12 ### Features From 47bcd9df8d275527e5f9cc5c1cdc2ccb53aaf38a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 26 Nov 2023 15:20:01 +0100 Subject: [PATCH 299/906] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Add=20support=20fo?= =?UTF-8?q?r=20Python=203.11=20and=20Python=203.12=20(#710)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 ++ docs/tutorial/index.md | 3 ++- pyproject.toml | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c3b07f484e..9f2688dff7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,6 +25,8 @@ jobs: - "3.8" - "3.9" - "3.10" + - "3.11" + - "3.12" fail-fast: false steps: diff --git a/docs/tutorial/index.md b/docs/tutorial/index.md index 74107776c2..773ab3b4a9 100644 --- a/docs/tutorial/index.md +++ b/docs/tutorial/index.md @@ -79,9 +79,10 @@ There's a chance that you have multiple Python versions installed. You might want to try with the specific versions, for example with: +* `python3.11` +* `python3.12` * `python3.10` * `python3.9` -* `python3.8` The code would look like this: diff --git a/pyproject.toml b/pyproject.toml index 515bbaf66c..9bfc434cfb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,8 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Database", "Topic :: Database :: Database Engines/Servers", "Topic :: Internet", From 65ee2610b65346c0b1d1be71ba05def93580bb1f Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 26 Nov 2023 14:20:17 +0000 Subject: [PATCH 300/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index c496468f49..b35c026df5 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,6 +6,10 @@ * ✅ Move OpenAPI tests inline to simplify updating them with Pydantic v2. PR [#709](https://github.com/tiangolo/sqlmodel/pull/709) by [@tiangolo](https://github.com/tiangolo). +### Upgrades + +* ⬆️ Add support for Python 3.11 and Python 3.12. PR [#710](https://github.com/tiangolo/sqlmodel/pull/710) by [@tiangolo](https://github.com/tiangolo). + ## 0.0.12 ### Features From f18ea03b07eec3921c9469d4df6297f0d063a8fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 27 Nov 2023 10:58:25 +0100 Subject: [PATCH 301/906] =?UTF-8?q?=F0=9F=99=88=20Update=20gitignore,=20in?= =?UTF-8?q?clude=20all=20coverage=20files=20(#711)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4006069389..65f9c629b1 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ poetry.lock dist htmlcov *.egg-info -.coverage +.coverage* coverage.xml site *.db From 71baff60151a5f4cf8b6b126dbaf991b2f00ed90 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 27 Nov 2023 09:58:47 +0000 Subject: [PATCH 302/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index b35c026df5..da3f72438f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest Changes +* 🙈 Update gitignore, include all coverage files. PR [#711](https://github.com/tiangolo/sqlmodel/pull/711) by [@tiangolo](https://github.com/tiangolo). + ### Refactors * ✅ Move OpenAPI tests inline to simplify updating them with Pydantic v2. PR [#709](https://github.com/tiangolo/sqlmodel/pull/709) by [@tiangolo](https://github.com/tiangolo). From a95bd3873d889850a68fca46b103467f6174b004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 28 Nov 2023 21:50:33 +0100 Subject: [PATCH 303/906] =?UTF-8?q?=F0=9F=94=A7=20Update=20config=20with?= =?UTF-8?q?=20new=20pymdown=20extensions=20(#712)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔧 Update config with new pymdown extensions * 📝 Update admonition blocks syntax * 📝 Update syntax for tabs with new pymdown extensions --- docs/advanced/decimal.md | 32 +++-- docs/databases.md | 30 ++-- docs/db-to-code.md | 28 ++-- docs/features.md | 11 +- docs/help.md | 20 ++- docs/tutorial/automatic-id-none-refresh.md | 9 +- docs/tutorial/code-structure.md | 9 +- .../tutorial/connect/create-connected-rows.md | 7 +- .../connect/create-connected-tables.md | 14 +- docs/tutorial/connect/index.md | 9 +- docs/tutorial/connect/read-connected-data.md | 45 ++++-- .../create-db-and-table-with-db-browser.md | 7 +- docs/tutorial/create-db-and-table.md | 135 ++++++++++++------ docs/tutorial/delete.md | 7 +- docs/tutorial/fastapi/limit-and-offset.md | 20 ++- docs/tutorial/fastapi/multiple-models.md | 16 ++- docs/tutorial/fastapi/read-one.md | 7 +- docs/tutorial/fastapi/response-model.md | 9 +- .../fastapi/session-with-dependency.md | 13 +- docs/tutorial/fastapi/simple-hero-api.md | 47 +++--- docs/tutorial/fastapi/tests.md | 51 +++++-- docs/tutorial/fastapi/update.md | 9 +- docs/tutorial/index.md | 112 ++++++++------- docs/tutorial/indexes.md | 20 ++- docs/tutorial/insert.md | 39 +++-- docs/tutorial/limit-and-offset.md | 16 ++- docs/tutorial/many-to-many/index.md | 32 +++-- .../many-to-many/link-with-extra-fields.md | 16 ++- .../update-remove-relationships.md | 9 +- docs/tutorial/one.md | 7 +- .../relationship-attributes/back-populates.md | 30 ++-- .../define-relationships-attributes.md | 9 +- .../tutorial/relationship-attributes/index.md | 9 +- .../read-relationships.md | 9 +- .../type-annotation-strings.md | 7 +- docs/tutorial/select.md | 59 +++++--- docs/tutorial/update.md | 51 ++++--- docs/tutorial/where.md | 80 +++++++---- mkdocs.yml | 29 +++- 39 files changed, 709 insertions(+), 360 deletions(-) diff --git a/docs/advanced/decimal.md b/docs/advanced/decimal.md index c0541b75df..3cd916399f 100644 --- a/docs/advanced/decimal.md +++ b/docs/advanced/decimal.md @@ -21,15 +21,21 @@ In most cases this would probably not be a problem, for example measuring views Pydantic has special support for `Decimal` types using the `condecimal()` special function. -!!! tip - Pydantic 1.9, that will be released soon, has improved support for `Decimal` types, without needing to use the `condecimal()` function. +/// tip - But meanwhile, you can already use this feature with `condecimal()` in **SQLModel** it as it's explained here. +Pydantic 1.9, that will be released soon, has improved support for `Decimal` types, without needing to use the `condecimal()` function. + +But meanwhile, you can already use this feature with `condecimal()` in **SQLModel** it as it's explained here. + +/// When you use `condecimal()` you can specify the number of digits and decimal places to support. They will be validated by Pydantic (for example when using FastAPI) and the same information will also be used for the database columns. -!!! info - For the database, **SQLModel** will use SQLAlchemy's `DECIMAL` type. +/// info + +For the database, **SQLModel** will use SQLAlchemy's `DECIMAL` type. + +/// ## Decimals in SQLModel @@ -72,8 +78,11 @@ We are also saying that the number of decimal places (to the right of the decima * `123` * Even though this number doesn't have any decimals, we still have 3 places saved for them, which means that we can **only use 2 places** for the **integer part**, and this number has 3 integer digits. So, the allowed number of integer digits is `max_digits` - `decimal_places` = 2. -!!! tip - Make sure you adjust the number of digits and decimal places for your own needs, in your own application. 🤓 +/// tip + +Make sure you adjust the number of digits and decimal places for your own needs, in your own application. 🤓 + +/// ## Create models with Decimals @@ -142,7 +151,10 @@ Total money: 3.300
-!!! warning - Although Decimal types are supported and used in the Python side, not all databases support it. In particular, SQLite doesn't support decimals, so it will convert them to the same floating `NUMERIC` type it supports. +/// warning + +Although Decimal types are supported and used in the Python side, not all databases support it. In particular, SQLite doesn't support decimals, so it will convert them to the same floating `NUMERIC` type it supports. + +But decimals are supported by most of the other SQL databases. 🎉 - But decimals are supported by most of the other SQL databases. 🎉 +/// diff --git a/docs/databases.md b/docs/databases.md index f1aaf663ab..54d5e4b18e 100644 --- a/docs/databases.md +++ b/docs/databases.md @@ -1,9 +1,12 @@ # Intro to Databases -!!! info - Are you a seasoned developer and already know everything about databases? 🤓 +/// info - Then you can skip to the [Tutorial - User Guide: First Steps](tutorial/index.md){.internal-link target=_blank} right away. +Are you a seasoned developer and already know everything about databases? 🤓 + +Then you can skip to the [Tutorial - User Guide: First Steps](tutorial/index.md){.internal-link target=_blank} right away. + +/// If you don't know everything about databases, here's a quick overview. @@ -17,8 +20,11 @@ So, what is a database? A **database** is a system to store and manage data in a structured and very efficient way. -!!! tip - It's very common to abbreviate the word "database" as **"DB"**. +/// tip + +It's very common to abbreviate the word "database" as **"DB"**. + +/// As there's a lot of information about databases, and it can get very technical and academic, I'll give you a quick overview about some of the main concepts here. @@ -28,8 +34,11 @@ I'll even tell you a bit about different types of databases, including the ones When starting to program, it might **not be obvious** why having a database apart from the code for your program is a **good idea**. Let's start with that. -!!! tip - If that's obvious to you, just continue in the next section below. 👇 +/// tip + +If that's obvious to you, just continue in the next section below. 👇 + +/// In your code you already have **variables**, **dictionaries**, **lists**, etc. They all store **data** in some way already. Why would you need to have a separate database? @@ -308,8 +317,11 @@ Next, it receives the data and puts it in Python objects that you can continue t I'll tell you more about SQL, SQLModel, how to use them, and how they are related in the next sections. -!!! info "Technical Details" - SQLModel is built on top of SQLAlchemy. It is, in fact, just SQLAlchemy and Pydantic mixed together with some sugar on top. +/// info | Technical Details + +SQLModel is built on top of SQLAlchemy. It is, in fact, just SQLAlchemy and Pydantic mixed together with some sugar on top. + +/// ## NoSQL Databases diff --git a/docs/db-to-code.md b/docs/db-to-code.md index 980c457148..537c583d3f 100644 --- a/docs/db-to-code.md +++ b/docs/db-to-code.md @@ -172,8 +172,11 @@ The difference in the final SQL statement is subtle, but it changes the meaning SELECT * FROM hero WHERE id = "2; DROP TABLE hero;"; ``` -!!! tip - Notice the double quotes (`"`) making it a string instead of more raw SQL. +/// tip + +Notice the double quotes (`"`) making it a string instead of more raw SQL. + +/// The database will not find any record with that ID: @@ -187,8 +190,11 @@ Then your code will continue to execute and calmly tell the user that it couldn' But we never deleted the `hero` table. 🎉 -!!! info - Of course, there are also other ways to do SQL data sanitization without using a tool like **SQLModel**, but it's still a nice feature you get by default. +/// info + +Of course, there are also other ways to do SQL data sanitization without using a tool like **SQLModel**, but it's still a nice feature you get by default. + +/// ### Editor Support @@ -291,8 +297,11 @@ There are many ORMs available apart from **SQLModel**, you can read more about s ## SQL Table Names -!!! info "Technical Background" - This is a bit of boring background for SQL purists. Feel free to skip this section. 😉 +/// info | Technical Background + +This is a bit of boring background for SQL purists. Feel free to skip this section. 😉 + +/// When working with pure SQL, it's common to name the tables in plural. So, the table would be named `heroes` instead of `hero`, because it could contain multiple rows, each with one hero. @@ -304,5 +313,8 @@ You will see **your own code** a lot more than the internal table names, so it's So, to keep things consistent, I'll keep using the same table names that **SQLModel** would have generated. -!!! tip - You can also override the table name. You can read about it in the Advanced User Guide. +/// tip + +You can also override the table name. You can read about it in the Advanced User Guide. + +/// diff --git a/docs/features.md b/docs/features.md index 102edef725..f84606b9b5 100644 --- a/docs/features.md +++ b/docs/features.md @@ -40,12 +40,15 @@ You won't need to keep guessing the types of different attributes in your models -!!! info - Don't worry, adopting this in-development standard only affects/improves editor support. +/// info - It doesn't affect performance or correctness. And if the in-progress standard was deprecated your code won't be affected. +Don't worry, adopting this in-development standard only affects/improves editor support. - Meanwhile, you will get inline errors (like type checks) and autocompletion on places you wouldn't get with any other library. 🎉 +It doesn't affect performance or correctness. And if the in-progress standard was deprecated your code won't be affected. + +Meanwhile, you will get inline errors (like type checks) and autocompletion on places you wouldn't get with any other library. 🎉 + +/// ## Short diff --git a/docs/help.md b/docs/help.md index d0d2308de2..8dc524b23b 100644 --- a/docs/help.md +++ b/docs/help.md @@ -157,12 +157,15 @@ And if there's any other style or consistency need, I'll ask directly for that, * Then **comment** saying that you did that, that's how I will know you really checked it. -!!! info - Unfortunately, I can't simply trust PRs that just have several approvals. +/// info - Several times it has happened that there are PRs with 3, 5 or more approvals, probably because the description is appealing, but when I check the PRs, they are actually broken, have a bug, or don't solve the problem they claim to solve. 😅 +Unfortunately, I can't simply trust PRs that just have several approvals. - So, it's really important that you actually read and run the code, and let me know in the comments that you did. 🤓 +Several times it has happened that there are PRs with 3, 5 or more approvals, probably because the description is appealing, but when I check the PRs, they are actually broken, have a bug, or don't solve the problem they claim to solve. 😅 + +So, it's really important that you actually read and run the code, and let me know in the comments that you did. 🤓 + +/// * If the PR can be simplified in a way, you can ask for that, but there's no need to be too picky, there might be a lot of subjective points of view (and I will have my own as well 🙈), so it's better if you can focus on the fundamental things. @@ -209,10 +212,13 @@ If you can help me with that, **you are helping me maintain SQLModel** and makin Join the 👥 FastAPI and Friends Discord chat server 👥 and hang out with others in the community. There's a `#sqlmodel` channel. -!!! tip - For questions, ask them in GitHub Discussions, there's a much better chance you will receive help there. +/// tip + +For questions, ask them in GitHub Discussions, there's a much better chance you will receive help there. + +Use the chat only for other general conversations. - Use the chat only for other general conversations. +/// ### Don't use the chat for questions diff --git a/docs/tutorial/automatic-id-none-refresh.md b/docs/tutorial/automatic-id-none-refresh.md index c7cf975ad4..e9ac7fdf0a 100644 --- a/docs/tutorial/automatic-id-none-refresh.md +++ b/docs/tutorial/automatic-id-none-refresh.md @@ -445,10 +445,13 @@ Hero 3: age=48 id=3 name='Rusty-Man' secret_name='Tommy Sharp' Now let's review all this code once again. -!!! tip - Each one of the numbered bubbles shows what each line will print in the output. +/// tip - And as we created the **engine** with `echo=True`, we can see the SQL statements being executed at each step. +Each one of the numbered bubbles shows what each line will print in the output. + +And as we created the **engine** with `echo=True`, we can see the SQL statements being executed at each step. + +/// ```{ .python .annotate } {!./docs_src/tutorial/automatic_id_none_refresh/tutorial002.py!} diff --git a/docs/tutorial/code-structure.md b/docs/tutorial/code-structure.md index 502c8bf958..0152ff669e 100644 --- a/docs/tutorial/code-structure.md +++ b/docs/tutorial/code-structure.md @@ -149,10 +149,13 @@ Let's say that for some reason you hate the idea of having all the database mode You can also do it. 😎 There's a couple of things to keep in mind. 🤓 -!!! warning - This is a bit more advanced. +/// warning - If the solution above already worked for you, that might be enough for you, and you can continue in the next chapter. 🤓 +This is a bit more advanced. + +If the solution above already worked for you, that might be enough for you, and you can continue in the next chapter. 🤓 + +/// Let's assume that now the file structure is: diff --git a/docs/tutorial/connect/create-connected-rows.md b/docs/tutorial/connect/create-connected-rows.md index beda24a515..0803432a28 100644 --- a/docs/tutorial/connect/create-connected-rows.md +++ b/docs/tutorial/connect/create-connected-rows.md @@ -37,8 +37,11 @@ Each row in the table `hero` will point to a row in the table `team`: table relationships -!!! info - We will later update **Spider-Boy** to add him to the **Preventers** team too, but not yet. +/// info + +We will later update **Spider-Boy** to add him to the **Preventers** team too, but not yet. + +/// We will continue with the code in the previous example and we will add more things to it. diff --git a/docs/tutorial/connect/create-connected-tables.md b/docs/tutorial/connect/create-connected-tables.md index 5a9a420a1b..5500fcc903 100644 --- a/docs/tutorial/connect/create-connected-tables.md +++ b/docs/tutorial/connect/create-connected-tables.md @@ -126,8 +126,11 @@ This is the name of the **table** in the database, so it is `"team"`, not the na If you had a custom table name, you would use that custom table name. -!!! info - You can learn about setting a custom table name for a model in the Advanced User Guide. +/// info + +You can learn about setting a custom table name for a model in the Advanced User Guide. + +/// ### Create the Tables @@ -167,8 +170,11 @@ And as before, we'll call this function from another function `main()`, and we'l ## Run the Code -!!! tip - Before running the code, make sure you delete the file `database.db` to make sure you start from scratch. +/// tip + +Before running the code, make sure you delete the file `database.db` to make sure you start from scratch. + +/// If we run the code we have up to now, it will go and create the database file `database.db` and the tables in it we just defined, `team` and `hero`: diff --git a/docs/tutorial/connect/index.md b/docs/tutorial/connect/index.md index 76e0c7bde4..aa57e432fa 100644 --- a/docs/tutorial/connect/index.md +++ b/docs/tutorial/connect/index.md @@ -6,7 +6,10 @@ But the main advantage and feature of SQL databases is being able to handle rela Let's see how to use **SQLModel** to manage connected data in the next chapters. 🤝 -!!! tip - We will extend this further in the next group of chapters making it even more convenient to work with in Python code, using **relationship attributes**. +/// tip - But you should start in this group of chapters first. 🤓 +We will extend this further in the next group of chapters making it even more convenient to work with in Python code, using **relationship attributes**. + +But you should start in this group of chapters first. 🤓 + +/// diff --git a/docs/tutorial/connect/read-connected-data.md b/docs/tutorial/connect/read-connected-data.md index 0ef3bccf43..91bafc47f2 100644 --- a/docs/tutorial/connect/read-connected-data.md +++ b/docs/tutorial/connect/read-connected-data.md @@ -62,8 +62,11 @@ FROM hero, team WHERE hero.team_id = team.id ``` -!!! info - Because we have two columns called `name`, one for `hero` and one for `team`, we can specify them with the prefix of the table name and the dot to make it explicit what we refer to. +/// info + +Because we have two columns called `name`, one for `hero` and one for `team`, we can specify them with the prefix of the table name and the dot to make it explicit what we refer to. + +/// Notice that now in the `WHERE` part we are not comparing one column with a literal value (like `hero.name = "Deadpond"`), but we are comparing two columns. @@ -99,14 +102,17 @@ You can go ahead and try it in **DB Browser for SQLite**: -!!! note - Wait, what about Spider-Boy? 😱 +/// note + +Wait, what about Spider-Boy? 😱 - He doesn't have a team, so his `team_id` is `NULL` in the database. And this SQL is comparing that `NULL` from the `team_id` with all the `id` fields in the rows in the `team` table. +He doesn't have a team, so his `team_id` is `NULL` in the database. And this SQL is comparing that `NULL` from the `team_id` with all the `id` fields in the rows in the `team` table. - As there's no team with an ID of `NULL`, it doesn't find a match. +As there's no team with an ID of `NULL`, it doesn't find a match. - But we'll see how to fix that later with a `LEFT JOIN`. +But we'll see how to fix that later with a `LEFT JOIN`. + +/// ## Select Related Data with **SQLModel** @@ -164,10 +170,13 @@ For each iteration in the `for` loop we get a a tuple with an instance of the cl And in this `for` loop we assign them to the variable `hero` and the variable `team`. -!!! info - There was a lot of research, design, and work behind **SQLModel** to make this provide the best possible developer experience. +/// info + +There was a lot of research, design, and work behind **SQLModel** to make this provide the best possible developer experience. - And you should get autocompletion and inline errors in your editor for both `hero` and `team`. 🎉 +And you should get autocompletion and inline errors in your editor for both `hero` and `team`. 🎉 + +/// ## Add It to Main @@ -281,10 +290,13 @@ Also in **DB Browser for SQLite**: -!!! tip - Why bother with all this if the result is the same? +/// tip + +Why bother with all this if the result is the same? + +This `JOIN` will be useful in a bit to be able to also get Spider-Boy, even if he doesn't have a team. - This `JOIN` will be useful in a bit to be able to also get Spider-Boy, even if he doesn't have a team. +/// ## Join Tables in **SQLModel** @@ -420,8 +432,11 @@ And that would return the following result, including **Spider-Boy** 🎉:
-!!! tip - The only difference between this query and the previous is that extra `LEFT OUTER`. +/// tip + +The only difference between this query and the previous is that extra `LEFT OUTER`. + +/// And here's another of the SQL variations, you could write `LEFT OUTER JOIN` or just `LEFT JOIN`, it means the same. diff --git a/docs/tutorial/create-db-and-table-with-db-browser.md b/docs/tutorial/create-db-and-table-with-db-browser.md index 4437f15a6d..72be6db297 100644 --- a/docs/tutorial/create-db-and-table-with-db-browser.md +++ b/docs/tutorial/create-db-and-table-with-db-browser.md @@ -42,8 +42,11 @@ Click the button New Database. A dialog should show up. Go to the [project directory you created](./index.md#create-a-project){.internal-link target=_blank} and save the file with a name of `database.db`. -!!! tip - It's common to save SQLite database files with an extension of `.db`. Sometimes also `.sqlite`. +/// tip + +It's common to save SQLite database files with an extension of `.db`. Sometimes also `.sqlite`. + +/// ## Create a Table diff --git a/docs/tutorial/create-db-and-table.md b/docs/tutorial/create-db-and-table.md index d0d27424d1..48341b96e0 100644 --- a/docs/tutorial/create-db-and-table.md +++ b/docs/tutorial/create-db-and-table.md @@ -33,8 +33,11 @@ The first thing we need to do is create a class to represent the data in the tab A class like this that represents some data is commonly called a **model**. -!!! tip - That's why this package is called `SQLModel`. Because it's mainly used to create **SQL Models**. +/// tip + +That's why this package is called `SQLModel`. Because it's mainly used to create **SQL Models**. + +/// For that, we will import `SQLModel` (plus other things we will also use) and create a class `Hero` that inherits from `SQLModel` and represents the **table model** for our heroes: @@ -57,10 +60,13 @@ This class `Hero` **represents the table** for our heroes. And each instance we We use the config `table=True` to tell **SQLModel** that this is a **table model**, it represents a table. -!!! info - It's also possible to have models without `table=True`, those would be only **data models**, without a table in the database, they would not be **table models**. +/// info + +It's also possible to have models without `table=True`, those would be only **data models**, without a table in the database, they would not be **table models**. - Those **data models** will be **very useful later**, but for now, we'll just keep adding the `table=True` configuration. +Those **data models** will be **very useful later**, but for now, we'll just keep adding the `table=True` configuration. + +/// ## Define the Fields, Columns @@ -112,8 +118,11 @@ And we also set the default value of `age` to `None`. -!!! tip - We also define `id` with `Optional`. But we will talk about `id` below. +/// tip + +We also define `id` with `Optional`. But we will talk about `id` below. + +/// This way, we tell **SQLModel** that `age` is not required when validating data and that it has a default value of `None`. @@ -121,10 +130,13 @@ And we also tell it that, in the SQL database, the default value of `age` is `NU So, this column is "nullable" (can be set to `NULL`). -!!! info - In terms of **Pydantic**, `age` is an **optional field**. +/// info + +In terms of **Pydantic**, `age` is an **optional field**. - In terms of **SQLAlchemy**, `age` is a **nullable column**. +In terms of **SQLAlchemy**, `age` is a **nullable column**. + +/// ### Primary Key `id` @@ -207,10 +219,13 @@ Creating the **engine** is very simple, just call `create_engine()` with a URL f You should normally have a single **engine** object for your whole application and re-use it everywhere. -!!! tip - There's another related thing called a **Session** that normally should *not* be a single object per application. +/// tip + +There's another related thing called a **Session** that normally should *not* be a single object per application. - But we will talk about it later. +But we will talk about it later. + +/// ### Engine Database URL @@ -272,8 +287,11 @@ engine = create_engine(sqlite_url) ### Engine Technical Details -!!! tip - If you didn't know about SQLAlchemy before and are just learning **SQLModel**, you can probably skip this section, scroll below. +/// tip + +If you didn't know about SQLAlchemy before and are just learning **SQLModel**, you can probably skip this section, scroll below. + +/// You can read a lot more about the engine in the SQLAlchemy documentation. @@ -289,12 +307,15 @@ Now everything is in place to finally create the database and table: {!./docs_src/tutorial/create_db_and_table/tutorial001.py!} ``` -!!! tip - Creating the engine doesn't create the `database.db` file. +/// tip + +Creating the engine doesn't create the `database.db` file. + +But once we run `SQLModel.metadata.create_all(engine)`, it creates the `database.db` file **and** creates the `hero` table in that database. - But once we run `SQLModel.metadata.create_all(engine)`, it creates the `database.db` file **and** creates the `hero` table in that database. +Both things are done in this single step. - Both things are done in this single step. +/// Let's unwrap that: @@ -404,8 +425,11 @@ Put the code it in a file `app.py` if you haven't already. -!!! tip - Remember to [activate the virtual environment](./index.md#create-a-python-virtual-environment){.internal-link target=_blank} before running it. +/// tip + +Remember to [activate the virtual environment](./index.md#create-a-python-virtual-environment){.internal-link target=_blank} before running it. + +/// Now run the program with Python: @@ -442,20 +466,23 @@ INFO Engine COMMIT -!!! info - I simplified the output above a bit to make it easier to read. +/// info - But in reality, instead of showing: +I simplified the output above a bit to make it easier to read. - ``` - INFO Engine BEGIN (implicit) - ``` +But in reality, instead of showing: - it would show something like: +``` +INFO Engine BEGIN (implicit) +``` + +it would show something like: + +``` +2021-07-25 21:37:39,175 INFO sqlalchemy.engine.Engine BEGIN (implicit) +``` - ``` - 2021-07-25 21:37:39,175 INFO sqlalchemy.engine.Engine BEGIN (implicit) - ``` +/// ### `TEXT` or `VARCHAR` @@ -479,8 +506,11 @@ Additional to the difference between those two data types, some databases like M To make it easier to start using **SQLModel** right away independent of the database you use (even with MySQL), and without any extra configurations, by default, `str` fields are interpreted as `VARCHAR` in most databases and `VARCHAR(255)` in MySQL, this way you know the same class will be compatible with the most popular databases without extra effort. -!!! tip - You will learn how to change the maximum length of string columns later in the Advanced Tutorial - User Guide. +/// tip + +You will learn how to change the maximum length of string columns later in the Advanced Tutorial - User Guide. + +/// ### Verify the Database @@ -519,8 +549,11 @@ We don't want that to happen like that, only when we **intend** it to happen, th Now we would be able to, for example, import the `Hero` class in some other file without having those **side effects**. -!!! tip - 😅 **Spoiler alert**: The function is called `create_db_and_tables()` because we will have more **tables** in the future with other classes apart from `Hero`. 🚀 +/// tip + +😅 **Spoiler alert**: The function is called `create_db_and_tables()` because we will have more **tables** in the future with other classes apart from `Hero`. 🚀 + +/// ### Create Data as a Script @@ -528,10 +561,13 @@ We prevented the side effects when importing something from your `app.py` file. But we still want it to **create the database and table** when we call it with Python directly as an independent script from the terminal, just as as above. -!!! tip - Think of the word **script** and **program** as interchangeable. +/// tip - The word **script** often implies that the code could be run independently and easily. Or in some cases it refers to a relatively simple program. +Think of the word **script** and **program** as interchangeable. + +The word **script** often implies that the code could be run independently and easily. Or in some cases it refers to a relatively simple program. + +/// For that we can use the special variable `__name__` in an `if` block: @@ -559,10 +595,13 @@ $ python app.py from app import Hero ``` -!!! tip - That `if` block using `if __name__ == "__main__":` is sometimes called the "**main block**". +/// tip + +That `if` block using `if __name__ == "__main__":` is sometimes called the "**main block**". + +The official name (in the Python docs) is "**Top-level script environment**". - The official name (in the Python docs) is "**Top-level script environment**". +/// #### More details @@ -614,8 +653,11 @@ if __name__ == "__main__": ...will **not** be executed. -!!! info - For more information, check the official Python docs. +/// info + +For more information, check the official Python docs. + +/// ## Last Review @@ -631,8 +673,11 @@ Now, let's give the code a final look: {!./docs_src/tutorial/create_db_and_table/annotations/en/tutorial003.md!} -!!! tip - Review what each line does by clicking each number bubble in the code. 👆 +/// tip + +Review what each line does by clicking each number bubble in the code. 👆 + +/// ## Recap diff --git a/docs/tutorial/delete.md b/docs/tutorial/delete.md index 590b2ece52..34dd3be423 100644 --- a/docs/tutorial/delete.md +++ b/docs/tutorial/delete.md @@ -333,8 +333,11 @@ Now let's review all that code: {!./docs_src/tutorial/delete/annotations/en/tutorial002.md!} -!!! tip - Check out the number bubbles to see what is done by each line of code. +/// tip + +Check out the number bubbles to see what is done by each line of code. + +/// ## Recap diff --git a/docs/tutorial/fastapi/limit-and-offset.md b/docs/tutorial/fastapi/limit-and-offset.md index 8802f4ec99..1152101eba 100644 --- a/docs/tutorial/fastapi/limit-and-offset.md +++ b/docs/tutorial/fastapi/limit-and-offset.md @@ -8,8 +8,11 @@ So, we probably want to limit it. Let's use the same **offset** and **limit** we learned about in the previous tutorial chapters for the API. -!!! info - In many cases, this is also called **pagination**. +/// info + +In many cases, this is also called **pagination**. + +/// ## Add a Limit and Offset to the Query Parameters @@ -46,12 +49,15 @@ So, to prevent it, we add additional validation to the `limit` query parameter, This way, a client can decide to take fewer heroes if they want, but not more. -!!! info - If you need to refresh how query parameters and their validation work, check out the docs in FastAPI: +/// info + +If you need to refresh how query parameters and their validation work, check out the docs in FastAPI: + +* Query Parameters +* Query Parameters and String Validations +* Path Parameters and Numeric Validations - * Query Parameters - * Query Parameters and String Validations - * Path Parameters and Numeric Validations +/// ## Check the Docs UI diff --git a/docs/tutorial/fastapi/multiple-models.md b/docs/tutorial/fastapi/multiple-models.md index 6845b9862d..3833f816a9 100644 --- a/docs/tutorial/fastapi/multiple-models.md +++ b/docs/tutorial/fastapi/multiple-models.md @@ -136,8 +136,11 @@ But `HeroCreate` and `HeroRead` don't have `table = True`. They are only **data This also means that `SQLModel.metadata.create_all()` won't create tables in the database for `HeroCreate` and `HeroRead`, because they don't have `table = True`, which is exactly what we want. 🚀 -!!! tip - We will improve this code to avoid duplicating the fields, but for now we can continue learning with these models. +/// tip + +We will improve this code to avoid duplicating the fields, but for now we can continue learning with these models. + +/// ## Use Multiple Models to Create a Hero @@ -208,10 +211,13 @@ And now that we return it, FastAPI will validate the data with the `response_mod This will validate that all the data that we promised is there and will remove any data we didn't declare. -!!! tip - This filtering could be very important and could be a very good security feature, for example, to make sure you filter private data, hashed passwords, etc. +/// tip + +This filtering could be very important and could be a very good security feature, for example, to make sure you filter private data, hashed passwords, etc. + +You can read more about it in the FastAPI docs about Response Model. - You can read more about it in the FastAPI docs about Response Model. +/// In particular, it will make sure that the `id` is there and that it is indeed an integer (and not `None`). diff --git a/docs/tutorial/fastapi/read-one.md b/docs/tutorial/fastapi/read-one.md index 8eea6488b1..b06ebc2a83 100644 --- a/docs/tutorial/fastapi/read-one.md +++ b/docs/tutorial/fastapi/read-one.md @@ -8,8 +8,11 @@ Let's add a new *path operation* to read one single hero. We want to get the hero based on the `id`, so we will use a **path parameter** `hero_id`. -!!! info - If you need to refresh how *path parameters* work, including their data validation, check the FastAPI docs about Path Parameters. +/// info + +If you need to refresh how *path parameters* work, including their data validation, check the FastAPI docs about Path Parameters. + +/// ```Python hl_lines="8" {!./docs_src/tutorial/fastapi/read_one/tutorial001.py[ln:1-4]!} diff --git a/docs/tutorial/fastapi/response-model.md b/docs/tutorial/fastapi/response-model.md index c019f4580b..29899beaff 100644 --- a/docs/tutorial/fastapi/response-model.md +++ b/docs/tutorial/fastapi/response-model.md @@ -100,10 +100,13 @@ Additionally, because the schemas are defined in using a standard, there are man For example, client generators, that can automatically create the code necessary to talk to your API in many languages. -!!! info - If you are curious about the standards, FastAPI generates OpenAPI, that internally uses JSON Schema. +/// info - You can read about all that in the FastAPI docs - First Steps. +If you are curious about the standards, FastAPI generates OpenAPI, that internally uses JSON Schema. + +You can read about all that in the FastAPI docs - First Steps. + +/// ## Recap diff --git a/docs/tutorial/fastapi/session-with-dependency.md b/docs/tutorial/fastapi/session-with-dependency.md index 195c2e1729..d4265af9f8 100644 --- a/docs/tutorial/fastapi/session-with-dependency.md +++ b/docs/tutorial/fastapi/session-with-dependency.md @@ -81,14 +81,17 @@ We import `Depends()` from `fastapi`. Then we use it in the *path operation func -!!! tip - Here's a tip about that `*,` thing in the parameters. +/// tip - Here we are passing the parameter `session` that has a "default value" of `Depends(get_session)` before the parameter `hero`, that doesn't have any default value. +Here's a tip about that `*,` thing in the parameters. - Python would normally complain about that, but we can use the initial "parameter" `*,` to mark all the rest of the parameters as "keyword only", which solves the problem. +Here we are passing the parameter `session` that has a "default value" of `Depends(get_session)` before the parameter `hero`, that doesn't have any default value. - You can read more about it in the FastAPI documentation Path Parameters and Numeric Validations - Order the parameters as you need, tricks +Python would normally complain about that, but we can use the initial "parameter" `*,` to mark all the rest of the parameters as "keyword only", which solves the problem. + +You can read more about it in the FastAPI documentation Path Parameters and Numeric Validations - Order the parameters as you need, tricks + +/// The value of a dependency will **only be used for one request**, FastAPI will call it right before calling your code and will give you the value from that dependency. diff --git a/docs/tutorial/fastapi/simple-hero-api.md b/docs/tutorial/fastapi/simple-hero-api.md index 53a5fa7d38..8debe579af 100644 --- a/docs/tutorial/fastapi/simple-hero-api.md +++ b/docs/tutorial/fastapi/simple-hero-api.md @@ -62,10 +62,13 @@ But here we will make sure we don't share the same **session** in more than one And we also need to disable it because in **FastAPI** each request could be handled by multiple interacting threads. -!!! info - That's enough information for now, you can read more about it in the FastAPI docs for `async` and `await`. +/// info - The main point is, by ensuring you **don't share** the same **session** with more than one request, the code is already safe. +That's enough information for now, you can read more about it in the FastAPI docs for `async` and `await`. + +The main point is, by ensuring you **don't share** the same **session** with more than one request, the code is already safe. + +/// ## **FastAPI** App @@ -119,8 +122,11 @@ This should be called only once at startup, not before every request, so we put ## Create Heroes *Path Operation* -!!! info - If you need a refresher on what a **Path Operation** is (an endpoint with a specific HTTP Operation) and how to work with it in FastAPI, check out the FastAPI First Steps docs. +/// info + +If you need a refresher on what a **Path Operation** is (an endpoint with a specific HTTP Operation) and how to work with it in FastAPI, check out the FastAPI First Steps docs. + +/// Let's create the **path operation** code to create a new hero. @@ -143,12 +149,15 @@ It will be called when a user sends a request with a `POST` **operation** to the -!!! info - If you need a refresher on some of those concepts, checkout the FastAPI documentation: +/// info + +If you need a refresher on some of those concepts, checkout the FastAPI documentation: - * First Steps - * Path Parameters - Data Validation and Data Conversion - * Request Body +* First Steps +* Path Parameters - Data Validation and Data Conversion +* Request Body + +/// ## The **SQLModel** Advantage @@ -162,8 +171,11 @@ And then, because this same **SQLModel** object is not only a **Pydantic** model So we can use intuitive standard Python **type annotations**, and we don't have to duplicate a lot of the code for the database models and the API data models. 🎉 -!!! tip - We will improve this further later, but for now, it already shows the power of having **SQLModel** classes be both **SQLAlchemy** models and **Pydantic** models at the same time. +/// tip + +We will improve this further later, but for now, it already shows the power of having **SQLModel** classes be both **SQLAlchemy** models and **Pydantic** models at the same time. + +/// ## Read Heroes *Path Operation* @@ -226,11 +238,14 @@ $ uvicorn main:app -!!! info - The command `uvicorn main:app` refers to: +/// info + +The command `uvicorn main:app` refers to: + +* `main`: the file `main.py` (the Python "module"). +* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. - * `main`: the file `main.py` (the Python "module"). - * `app`: the object created inside of `main.py` with the line `app = FastAPI()`. +/// ### Uvicorn `--reload` diff --git a/docs/tutorial/fastapi/tests.md b/docs/tutorial/fastapi/tests.md index cc6ad65c6f..ea084335b4 100644 --- a/docs/tutorial/fastapi/tests.md +++ b/docs/tutorial/fastapi/tests.md @@ -71,8 +71,11 @@ Let's start with a simple test, with just the basic test code we need the check {!./docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_001.md!} -!!! tip - Check out the number bubbles to see what is done by each line of code. +/// tip + +Check out the number bubbles to see what is done by each line of code. + +/// That's the **core** of the code we need for all the tests later. @@ -116,8 +119,11 @@ That way we protect the production database and we have better control of the da {!./docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_002.md!} -!!! tip - Check out the number bubbles to see what is done by each line of code. +/// tip + +Check out the number bubbles to see what is done by each line of code. + +/// ## Create the Engine and Session for Testing @@ -197,8 +203,11 @@ We just have to change a couple of parameters in the **engine**. {!./docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_004.md!} -!!! tip - Check out the number bubbles to see what is done by each line of code. +/// tip + +Check out the number bubbles to see what is done by each line of code. + +/// That's it, now the test will run using the **in-memory database**, which will be faster and probably safer. @@ -214,8 +223,11 @@ Do we really have to duplicate all that for **each test**? No, we can do better! We are using **pytest** to run the tests. And pytest also has a very similar concept to the **dependencies in FastAPI**. -!!! info - In fact, pytest was one of the things that inspired the design of the dependencies in FastAPI. +/// info + +In fact, pytest was one of the things that inspired the design of the dependencies in FastAPI. + +/// It's a way for us to declare some **code that should be run before** each test and **provide a value** for the test function (that's pretty much the same as FastAPI dependencies). @@ -237,8 +249,11 @@ Let's see the first code example with a fixture: {!./docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_005.md!} -!!! tip - Check out the number bubbles to see what is done by each line of code. +/// tip + +Check out the number bubbles to see what is done by each line of code. + +/// **pytest** fixtures work in a very similar way to FastAPI dependencies, but have some minor differences: @@ -274,8 +289,11 @@ So, we can create a **client fixture** that will be used in all the tests, and i {!./docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_006.md!} -!!! tip - Check out the number bubbles to see what is done by each line of code. +/// tip + +Check out the number bubbles to see what is done by each line of code. + +/// Now we have a **client fixture** that, in turn, uses the **session fixture**. @@ -306,10 +324,13 @@ Let's add some more tests: -!!! tip - It's always **good idea** to not only test the normal case, but also that **invalid data**, **errors**, and **corner cases** are handled correctly. +/// tip + +It's always **good idea** to not only test the normal case, but also that **invalid data**, **errors**, and **corner cases** are handled correctly. + +That's why we add these two extra tests here. - That's why we add these two extra tests here. +/// Now, any additional test functions can be as **simple** as the first one, they just have to **declare the `client` parameter** to get the `TestClient` **fixture** with all the database stuff setup. Nice! 😎 diff --git a/docs/tutorial/fastapi/update.md b/docs/tutorial/fastapi/update.md index 0b5292bd29..df04600510 100644 --- a/docs/tutorial/fastapi/update.md +++ b/docs/tutorial/fastapi/update.md @@ -12,10 +12,13 @@ So, we need to have all those fields **marked as optional**. And because the `HeroBase` has some of them as *required* and not optional, we will need to **create a new model**. -!!! tip - Here is one of those cases where it probably makes sense to use an **independent model** instead of trying to come up with a complex tree of models inheriting from each other. +/// tip - Because each field is **actually different** (we just change it to `Optional`, but that's already making it different), it makes sense to have them in their own model. +Here is one of those cases where it probably makes sense to use an **independent model** instead of trying to come up with a complex tree of models inheriting from each other. + +Because each field is **actually different** (we just change it to `Optional`, but that's already making it different), it makes sense to have them in their own model. + +/// So, let's create this new `HeroUpdate` model: diff --git a/docs/tutorial/index.md b/docs/tutorial/index.md index 773ab3b4a9..1e78c9c4f7 100644 --- a/docs/tutorial/index.md +++ b/docs/tutorial/index.md @@ -57,8 +57,11 @@ $ cd sqlmodel-tutorial -!!! tip - Make sure you don't name it also `sqlmodel`, so that you don't end up overriding the name of the package. +/// tip + +Make sure you don't name it also `sqlmodel`, so that you don't end up overriding the name of the package. + +/// ### Make sure you have Python @@ -119,61 +122,68 @@ In very short, a virtual environment is a small directory that contains a copy o And when you "activate" it, any package that you install, for example with `pip`, will be installed in that virtual environment. -!!! tip - There are other tools to manage virtual environments, like Poetry. +/// tip + +There are other tools to manage virtual environments, like Poetry. - And there are alternatives that are particularly useful for deployment like Docker and other types of containers. In this case, the "virtual environment" is not just the Python standard files and the installed packages, but the whole system. +And there are alternatives that are particularly useful for deployment like Docker and other types of containers. In this case, the "virtual environment" is not just the Python standard files and the installed packages, but the whole system. + +/// Go ahead and create a Python virtual environment for this project. And make sure to also upgrade `pip`. Here are the commands you could use: -=== "Linux, macOS, Linux in Windows" - -
- - ```console - // Remember that you might need to use python3.9 or similar 💡 - // Create the virtual environment using the module "venv" - $ python3 -m venv env - // ...here it creates the virtual environment in the directory "env" - // Activate the virtual environment - $ source ./env/bin/activate - // Verify that the virtual environment is active - # (env) $$ which python - // The important part is that it is inside the project directory, at "code/sqlmodel-tutorial/env/bin/python" - /home/leela/code/sqlmodel-tutorial/env/bin/python - // Use the module "pip" to install and upgrade the package "pip" 🤯 - # (env) $$ python -m pip install --upgrade pip - ---> 100% - Successfully installed pip - ``` - -
- -=== "Windows PowerShell" - -
- - ```console - // Create the virtual environment using the module "venv" - # >$ python3 -m venv env - // ...here it creates the virtual environment in the directory "env" - // Activate the virtual environment - # >$ .\env\Scripts\Activate.ps1 - // Verify that the virtual environment is active - # (env) >$ Get-Command python - // The important part is that it is inside the project directory, at "code\sqlmodel-tutorial\env\python.exe" - CommandType Name Version Source - ----------- ---- ------- ------ - Application python 0.0.0.0 C:\Users\leela\code\sqlmodel-tutorial\env\python.exe - // Use the module "pip" to install and upgrade the package "pip" 🤯 - # (env) >$ python3 -m pip install --upgrade pip - ---> 100% - Successfully installed pip - ``` - -
+/// tab | Linux, macOS, Linux in Windows + +
+ +```console +// Remember that you might need to use python3.9 or similar 💡 +// Create the virtual environment using the module "venv" +$ python3 -m venv env +// ...here it creates the virtual environment in the directory "env" +// Activate the virtual environment +$ source ./env/bin/activate +// Verify that the virtual environment is active +# (env) $$ which python +// The important part is that it is inside the project directory, at "code/sqlmodel-tutorial/env/bin/python" +/home/leela/code/sqlmodel-tutorial/env/bin/python +// Use the module "pip" to install and upgrade the package "pip" 🤯 +# (env) $$ python -m pip install --upgrade pip +---> 100% +Successfully installed pip +``` + +
+ +/// + +/// tab | Windows PowerShell + +
+ +```console +// Create the virtual environment using the module "venv" +# >$ python3 -m venv env +// ...here it creates the virtual environment in the directory "env" +// Activate the virtual environment +# >$ .\env\Scripts\Activate.ps1 +// Verify that the virtual environment is active +# (env) >$ Get-Command python +// The important part is that it is inside the project directory, at "code\sqlmodel-tutorial\env\python.exe" +CommandType Name Version Source +----------- ---- ------- ------ +Application python 0.0.0.0 C:\Users\leela\code\sqlmodel-tutorial\env\python.exe +// Use the module "pip" to install and upgrade the package "pip" 🤯 +# (env) >$ python3 -m pip install --upgrade pip +---> 100% +Successfully installed pip +``` + +
+ +/// ## Install **SQLModel** diff --git a/docs/tutorial/indexes.md b/docs/tutorial/indexes.md index fef0081dc8..9a4be7dbfb 100644 --- a/docs/tutorial/indexes.md +++ b/docs/tutorial/indexes.md @@ -73,12 +73,15 @@ You repeat this process **a few more times**, and you finally arrive at the lett You had to open the dictionary a few times, maybe **5 or 10**. That's actually **very little work** compared to what it could have been. -!!! note "Technical Details" - Do you like **fancy words**? Cool! Programmers tend to like fancy words. 😅 +/// note | Technical Details - That algorithm I showed you above is called **Binary Search**. +Do you like **fancy words**? Cool! Programmers tend to like fancy words. 😅 - It's called like that because you **search** something by splitting the dictionary (or any ordered list of things) in **two** ("binary" means "two") parts. And you do that process multiple times until you find what you want. +That algorithm I showed you above is called **Binary Search**. + +It's called like that because you **search** something by splitting the dictionary (or any ordered list of things) in **two** ("binary" means "two") parts. And you do that process multiple times until you find what you want. + +/// ### An Index and a Novel @@ -297,10 +300,13 @@ We use the same `Field()` again as we did before, and set `index=True`. That's i Notice that we didn't set an argument of `default=None` or anything similar. This means that **SQLModel** (thanks to Pydantic) will keep it as a **required** field. -!!! info - SQLModel (actually SQLAlchemy) will **automatically generate the index name** for you. +/// info + +SQLModel (actually SQLAlchemy) will **automatically generate the index name** for you. + +In this case the generated name would be `ix_hero_name`. - In this case the generated name would be `ix_hero_name`. +/// ## Query Data diff --git a/docs/tutorial/insert.md b/docs/tutorial/insert.md index ecf87adbad..31b387dced 100644 --- a/docs/tutorial/insert.md +++ b/docs/tutorial/insert.md @@ -70,8 +70,11 @@ You can try that SQL statement in **DB Explorer for SQLite**. Make sure to open the same database we already created by clicking Open Database and selecting the same `database.db` file. -!!! tip - If you don't have that `database.db` file with the table `hero`, you can re-create it by running the Python program at the top. 👆 +/// tip + +If you don't have that `database.db` file with the table `hero`, you can re-create it by running the Python program at the top. 👆 + +/// Then go to the Execute SQL tab and copy the SQL from above. @@ -141,10 +144,13 @@ We'll create 3 right away, for the 3 heroes: -!!! tip - The code above in this file (the omitted code) is just the same code that you see at the top of this chapter. +/// tip + +The code above in this file (the omitted code) is just the same code that you see at the top of this chapter. - The same code we used before to create the `Hero` model. +The same code we used before to create the `Hero` model. + +/// We are putting that in a function `create_heroes()`, to call it later once we finish it. @@ -204,8 +210,11 @@ Then we can create a new session: The new `Session` takes an `engine` as a parameter. And it will use the **engine** underneath. -!!! tip - We will see a better way to create a **session** using a `with` block later. +/// tip + +We will see a better way to create a **session** using a `with` block later. + +/// ## Add Model Instances to the Session @@ -237,10 +246,13 @@ And once we are ready, we can **commit** those changes, and then the **session** This makes the interactions with the database more efficient (plus some extra benefits). -!!! info "Technical Details" - The session will create a new transaction and execute all the SQL code in that transaction. +/// info | Technical Details + +The session will create a new transaction and execute all the SQL code in that transaction. + +This ensures that the data is saved in a single batch, and that it will all succeed or all fail, but it won't leave the database in a broken state. - This ensures that the data is saved in a single batch, and that it will all succeed or all fail, but it won't leave the database in a broken state. +/// ## Commit the Session Changes @@ -433,8 +445,11 @@ Let's focus on the new code: {!./docs_src/tutorial/insert/annotations/en/tutorial003.md!} -!!! tip - Review what each line does by clicking each number bubble in the code. 👆 +/// tip + +Review what each line does by clicking each number bubble in the code. 👆 + +/// You can now put it in a `app.py` file and run it with Python. And you will see an output like the one shown above. diff --git a/docs/tutorial/limit-and-offset.md b/docs/tutorial/limit-and-offset.md index c8b0ddf72f..bcd17b2f8f 100644 --- a/docs/tutorial/limit-and-offset.md +++ b/docs/tutorial/limit-and-offset.md @@ -110,8 +110,11 @@ INFO Engine [no key 0.00014s] (3, 0) Great! We got only 3 heroes as we wanted. -!!! tip - We will check out that SQL code more in a bit. +/// tip + +We will check out that SQL code more in a bit. + +/// ## Select with Offset and Limit @@ -119,10 +122,13 @@ Now we can limit the results to get only the first 3. But imagine we are in a user interface showing the results in batches of 3 heroes at a time. -!!! tip - This is commonly called "pagination". Because the user interface would normally show a "page" of a predefined number of heroes at a time. +/// tip + +This is commonly called "pagination". Because the user interface would normally show a "page" of a predefined number of heroes at a time. + +And then you can interact with the user interface to get the next page, and so on. - And then you can interact with the user interface to get the next page, and so on. +/// How do we get the next 3? diff --git a/docs/tutorial/many-to-many/index.md b/docs/tutorial/many-to-many/index.md index e2e34777c0..2aa2cf52f9 100644 --- a/docs/tutorial/many-to-many/index.md +++ b/docs/tutorial/many-to-many/index.md @@ -30,8 +30,11 @@ The `team` table looks like this: -!!! tip - Notice that it doesn't have any foreign key to other tables. +/// tip + +Notice that it doesn't have any foreign key to other tables. + +/// And the `hero` table looks like this: @@ -106,19 +109,22 @@ Specifically, the new link table `heroteamlink` would be: -!!! info - Other names used for this **link table** are: +/// info + +Other names used for this **link table** are: + +* association table +* secondary table +* junction table +* intermediate table +* join table +* through table +* relationship table +* connection table - * association table - * secondary table - * junction table - * intermediate table - * join table - * through table - * relationship table - * connection table +I'm using the term "link table" because it's short, doesn't collide with other terms already used (e.g. "relationship"), it's easy to remember how to write it, etc. - I'm using the term "link table" because it's short, doesn't collide with other terms already used (e.g. "relationship"), it's easy to remember how to write it, etc. +/// ## Link Primary Key diff --git a/docs/tutorial/many-to-many/link-with-extra-fields.md b/docs/tutorial/many-to-many/link-with-extra-fields.md index c998175a72..90839c61c6 100644 --- a/docs/tutorial/many-to-many/link-with-extra-fields.md +++ b/docs/tutorial/many-to-many/link-with-extra-fields.md @@ -18,8 +18,11 @@ A row in the table `heroteamlink` points to **one** particular hero, but a singl And also, the same row in the table `heroteamlink` points to **one** team, but a single team can be connected to **many** hero-team links, so it's also **one-to-many**. -!!! tip - The previous many-to-many relationship was also just two one-to-many relationships combined, but now it's going to be much more explicit. +/// tip + +The previous many-to-many relationship was also just two one-to-many relationships combined, but now it's going to be much more explicit. + +/// ## Update Link Model @@ -51,10 +54,13 @@ The new **relationship attributes** have their own `back_populates` pointing to * `team`: has `back_populates="hero_links"`, because in the `Team` model, the attribute will contain the links to the **team's heroes**. * `hero`: has `back_populates="team_links"`, because in the `Hero` model, the attribute will contain the links to the **hero's teams**. -!!! info - In SQLAlchemy this is called an Association Object or Association Model. +/// info + +In SQLAlchemy this is called an Association Object or Association Model. + +I'm calling it **Link Model** just because that's easier to write avoiding typos. But you are also free to call it however you want. 😉 - I'm calling it **Link Model** just because that's easier to write avoiding typos. But you are also free to call it however you want. 😉 +/// ## Update Team Model diff --git a/docs/tutorial/many-to-many/update-remove-relationships.md b/docs/tutorial/many-to-many/update-remove-relationships.md index 91f2f2c0e2..f051c3c190 100644 --- a/docs/tutorial/many-to-many/update-remove-relationships.md +++ b/docs/tutorial/many-to-many/update-remove-relationships.md @@ -82,10 +82,13 @@ We can use the same **relationship attributes** to include `hero_spider_boy` in -!!! tip - Because we are accessing an attribute in the models right after we commit, with `hero_spider_boy.teams` and `team_z_force.heroes`, the data is refreshed automatically. +/// tip - So we don't have to call `session.refresh()`. +Because we are accessing an attribute in the models right after we commit, with `hero_spider_boy.teams` and `team_z_force.heroes`, the data is refreshed automatically. + +So we don't have to call `session.refresh()`. + +/// We then commit the change, refresh, and print the updated **Spider-Boy**'s heroes to confirm. diff --git a/docs/tutorial/one.md b/docs/tutorial/one.md index eadfc62a37..6419d9cd71 100644 --- a/docs/tutorial/one.md +++ b/docs/tutorial/one.md @@ -71,8 +71,11 @@ This will return the first object in the `results` (if there was any). That way, we don't have to deal with an iterable or a list. -!!! tip - Notice that `.first()` is a method of the `results` object, not of the `select()` statement. +/// tip + +Notice that `.first()` is a method of the `results` object, not of the `select()` statement. + +/// Although this query would find two rows, by using `.first()` we get only the first row. diff --git a/docs/tutorial/relationship-attributes/back-populates.md b/docs/tutorial/relationship-attributes/back-populates.md index dbd3e8c1a3..e56bcc2460 100644 --- a/docs/tutorial/relationship-attributes/back-populates.md +++ b/docs/tutorial/relationship-attributes/back-populates.md @@ -58,8 +58,11 @@ As you already know how this works, I won't separate that in a select `statement -!!! tip - When writing your own code, this is probably the style you will use most often, as it's shorter, more convenient, and you still get all the power of autocompletion and inline errors. +/// tip + +When writing your own code, this is probably the style you will use most often, as it's shorter, more convenient, and you still get all the power of autocompletion and inline errors. + +/// ## Print the Data @@ -127,8 +130,11 @@ The first important thing is, we *haven't committed* the hero yet, so accessing But in our code, in this exact point in time, we already said that **Spider-Boy** is no longer part of the **Preventers**. 🔥 -!!! tip - We could revert that later by not committing the **session**, but that's not what we are interested in here. +/// tip + +We could revert that later by not committing the **session**, but that's not what we are interested in here. + +/// Here, at this point in the code, in memory, the code expects **Preventers** to *not include* **Spider-Boy**. @@ -247,10 +253,13 @@ And we can keep the rest of the code the same: -!!! tip - This is the same section where we updated `hero_spider_boy.team` to `None` but we *haven't committed* that change yet. +/// tip - The same section that caused a problem before. +This is the same section where we updated `hero_spider_boy.team` to `None` but we *haven't committed* that change yet. + +The same section that caused a problem before. + +/// ## Review the Result @@ -336,8 +345,11 @@ So, the string `"heroes"` refers to the attribute `heroes` in the class `Team`. -!!! tip - Each **relationship attribute** points to the other one, in the other model, using `back_populates`. +/// tip + +Each **relationship attribute** points to the other one, in the other model, using `back_populates`. + +/// Although it's simple code, it can be confusing to think about 😵, because the same line has concepts related to both models in multiple places: diff --git a/docs/tutorial/relationship-attributes/define-relationships-attributes.md b/docs/tutorial/relationship-attributes/define-relationships-attributes.md index b6e77d9b45..0c2b4f1ef7 100644 --- a/docs/tutorial/relationship-attributes/define-relationships-attributes.md +++ b/docs/tutorial/relationship-attributes/define-relationships-attributes.md @@ -123,10 +123,13 @@ And in the `Team` class, the `heroes` attribute is annotated as a list of `Hero` **SQLModel** (actually SQLAlchemy) is smart enough to know that the relationship is established by the `team_id`, as that's the foreign key that points from the `hero` table to the `team` table, so we don't have to specify that explicitly here. -!!! tip - There's a couple of things we'll check again in some of the next chapters, about the `List["Hero"]` and the `back_populates`. +/// tip - But for now, let's first see how to use these relationship attributes. +There's a couple of things we'll check again in some of the next chapters, about the `List["Hero"]` and the `back_populates`. + +But for now, let's first see how to use these relationship attributes. + +/// ## Next Steps diff --git a/docs/tutorial/relationship-attributes/index.md b/docs/tutorial/relationship-attributes/index.md index 2b8b843bbd..b32fb637df 100644 --- a/docs/tutorial/relationship-attributes/index.md +++ b/docs/tutorial/relationship-attributes/index.md @@ -6,9 +6,12 @@ And then we read the data together with `select()` and using `.where()` or `.joi Now we will see how to use **Relationship Attributes**, an extra feature of **SQLModel** (and SQLAlchemy) to work with the data in the database in way much more familiar way, and closer to normal Python code. -!!! info - When I say "**relationship**" I mean the standard dictionary term, of data related to other data. +/// info - I'm not using the term "**relation**" that is the technical, academical, SQL term for a single table. +When I say "**relationship**" I mean the standard dictionary term, of data related to other data. + +I'm not using the term "**relation**" that is the technical, academical, SQL term for a single table. + +/// And using those **relationship attributes** is where a tool like **SQLModel** really shines. ✨ diff --git a/docs/tutorial/relationship-attributes/read-relationships.md b/docs/tutorial/relationship-attributes/read-relationships.md index 78e4207ae5..3a281a2038 100644 --- a/docs/tutorial/relationship-attributes/read-relationships.md +++ b/docs/tutorial/relationship-attributes/read-relationships.md @@ -77,10 +77,13 @@ So, the highlighted block above, has the same results as the block below: -!!! tip - The automatic data fetching will work as long as the starting object (in this case the `Hero`) is associated with an **open** session. +/// tip - For example, here, **inside** a `with` block with a `Session` object. +The automatic data fetching will work as long as the starting object (in this case the `Hero`) is associated with an **open** session. + +For example, here, **inside** a `with` block with a `Session` object. + +/// ## Get a List of Relationship Objects diff --git a/docs/tutorial/relationship-attributes/type-annotation-strings.md b/docs/tutorial/relationship-attributes/type-annotation-strings.md index da77dad6bb..e51e71c106 100644 --- a/docs/tutorial/relationship-attributes/type-annotation-strings.md +++ b/docs/tutorial/relationship-attributes/type-annotation-strings.md @@ -29,5 +29,8 @@ And of course, **SQLModel** can also understand it in the string correctly. ✨ That is actually part of Python, it's the current official solution to handle it. -!!! info - There's a lot of work going on in Python itself to make that simpler and more intuitive, and find ways to make it possible to not wrap the class in a string. +/// info + +There's a lot of work going on in Python itself to make that simpler and more intuitive, and find ways to make it possible to not wrap the class in a string. + +/// diff --git a/docs/tutorial/select.md b/docs/tutorial/select.md index e2f9af447c..43003231c9 100644 --- a/docs/tutorial/select.md +++ b/docs/tutorial/select.md @@ -79,12 +79,15 @@ You can try that out in **DB Browser for SQLite**: -!!! warning - Here we are getting all the rows. +/// warning - If you have thousands of rows, that could be expensive to compute for the database. +Here we are getting all the rows. - You would normally want to filter the rows to receive only the ones you want. But we'll learn about that later in the next chapter. +If you have thousands of rows, that could be expensive to compute for the database. + +You would normally want to filter the rows to receive only the ones you want. But we'll learn about that later in the next chapter. + +/// ### A SQL Shortcut @@ -240,10 +243,13 @@ We pass the class model `Hero` to the `select()` function. And that tells it tha And notice that in the `select()` function we don't explicitly specify the `FROM` part. It is already obvious to **SQLModel** (actually to SQLAlchemy) that we want to select `FROM` the table `hero`, because that's the one associated with the `Hero` class model. -!!! tip - The value of the `statement` returned by `select()` is a special object that allows us to do other things. +/// tip + +The value of the `statement` returned by `select()` is a special object that allows us to do other things. + +I'll tell you about that in the next chapters. - I'll tell you about that in the next chapters. +/// ## Execute the Statement @@ -360,8 +366,11 @@ Let's review the code up to this point: {!./docs_src/tutorial/select/annotations/en/tutorial002.md!} -!!! tip - Check out the number bubbles to see what is done by each line of code. +/// tip + +Check out the number bubbles to see what is done by each line of code. + +/// Here it starts to become more evident why we should have a single **engine** for the whole application, but different **sessions** for each group of operations. @@ -373,10 +382,13 @@ And the second section reading data from the database could be in another functi So, both sections could be in **different places** and would need their own sessions. -!!! info - To be fair, in this example all that code could actually share the same **session**, there's actually no need to have two here. +/// info + +To be fair, in this example all that code could actually share the same **session**, there's actually no need to have two here. - But it allows me to show you how they could be separated and to reinforce the idea that you should have **one engine** per application, and **multiple sessions**, one per each group of operations. +But it allows me to show you how they could be separated and to reinforce the idea that you should have **one engine** per application, and **multiple sessions**, one per each group of operations. + +/// ## Get a List of `Hero` Objects @@ -415,8 +427,11 @@ After printing it, we would see something like: ] ``` -!!! info - It would actually look more compact, I'm formatting it a bit for you to see that it is actually a list with all the data. +/// info + +It would actually look more compact, I'm formatting it a bit for you to see that it is actually a list with all the data. + +/// ## Compact Version @@ -461,8 +476,11 @@ SQLAchemy also has it's own `select`, and SQLModel's `select` uses SQLAlchemy's But SQLModel's version does a lot of **tricks** with type annotations to make sure you get the best **editor support** possible, no matter if you use **VS Code**, **PyCharm**, or something else. ✨ -!!! info - There was a lot of work and research, with different versions of the internal code, to improve this as much as possible. 🤓 +/// info + +There was a lot of work and research, with different versions of the internal code, to improve this as much as possible. 🤓 + +/// ### SQLModel's `session.exec` @@ -492,10 +510,13 @@ On top of that, **SQLModel**'s `session.exec()` also does some tricks to reduce But SQLModel's `Session` still has access to `session.execute()` too. -!!! tip - Your editor will give you autocompletion for both `session.exec()` and `session.execute()`. +/// tip + +Your editor will give you autocompletion for both `session.exec()` and `session.execute()`. + +📢 Remember to **always use `session.exec()`** to get the best editor support and developer experience. - 📢 Remember to **always use `session.exec()`** to get the best editor support and developer experience. +/// ### Caveats of **SQLModel** Flavor diff --git a/docs/tutorial/update.md b/docs/tutorial/update.md index 56cc7ac1a5..47ee881adc 100644 --- a/docs/tutorial/update.md +++ b/docs/tutorial/update.md @@ -41,12 +41,15 @@ And the second part, with the `WHERE`, defines to which rows it should apply tha In this case, as we only have one hero with the name `"Spider-Boy"`, it will only apply the update in that row. -!!! info - Notice that in the `UPDATE` the single equals sign (`=`) means **assignment**, setting a column to some value. +/// info - And in the `WHERE` the same single equals sign (`=`) is used for **comparison** between two values, to find rows that match. +Notice that in the `UPDATE` the single equals sign (`=`) means **assignment**, setting a column to some value. - This is in contrast to Python and most programming languages, where a single equals sign (`=`) is used for assignment, and two equal signs (`==`) are used for comparisons. +And in the `WHERE` the same single equals sign (`=`) is used for **comparison** between two values, to find rows that match. + +This is in contrast to Python and most programming languages, where a single equals sign (`=`) is used for assignment, and two equal signs (`==`) are used for comparisons. + +/// You can try that in **DB Browser for SQLite**: @@ -69,16 +72,19 @@ After that update, the data in the table will look like this, with the new age f -!!! tip - It will probably be more common to find the row to update by `id`, for example: +/// tip + +It will probably be more common to find the row to update by `id`, for example: + +```SQL +UPDATE hero +SET age=16 +WHERE id = 2 +``` - ```SQL - UPDATE hero - SET age=16 - WHERE id = 2 - ``` +But in the example above I used `name` to make it more intuitive. - But in the example above I used `name` to make it more intuitive. +/// Now let's do the same update in code, with **SQLModel**. @@ -143,8 +149,11 @@ Hero: name='Spider-Boy' secret_name='Pedro Parqueador' age=None id=2 -!!! tip - Notice that by this point, the hero still doesn't have an age. +/// tip + +Notice that by this point, the hero still doesn't have an age. + +/// ## Set a Field Value @@ -333,8 +342,11 @@ Now let's review all that code: {!./docs_src/tutorial/update/annotations/en/tutorial002.md!} -!!! tip - Check out the number bubbles to see what is done by each line of code. +/// tip + +Check out the number bubbles to see what is done by each line of code. + +/// ## Multiple Updates @@ -361,8 +373,11 @@ This also means that you can update several fields (attributes, columns) at once -!!! tip - Review what each line does by clicking each number bubble in the code. 👆 +/// tip + +Review what each line does by clicking each number bubble in the code. 👆 + +/// ## Recap diff --git a/docs/tutorial/where.md b/docs/tutorial/where.md index a3bf6b0529..541b0833bc 100644 --- a/docs/tutorial/where.md +++ b/docs/tutorial/where.md @@ -81,10 +81,13 @@ Then the database will bring a table like this: -!!! tip - Even if the result is only one row, the database always returns a **table**. +/// tip - In this case, a table with only one row. +Even if the result is only one row, the database always returns a **table**. + +In this case, a table with only one row. + +/// You can try that out in **DB Browser for SQLite**: @@ -268,10 +271,13 @@ So, what's happening there? In the example above we are using two equal signs (`==`). That's called the "**equality operator**". -!!! tip - An **operator** is just a symbol that is put beside one value or in the middle of two values to do something with them. +/// tip + +An **operator** is just a symbol that is put beside one value or in the middle of two values to do something with them. - `==` is called the **equality** operator because it checks if two things are **equal**. +`==` is called the **equality** operator because it checks if two things are **equal**. + +/// When writing Python, if you write something using this equality operator (`==`) like: @@ -291,8 +297,11 @@ True False ``` -!!! tip - `<`, `>`, `==`, `>=`, `<=`, and `!=` are all **operators** used for **comparisons**. +/// tip + +`<`, `>`, `==`, `>=`, `<=`, and `!=` are all **operators** used for **comparisons**. + +/// But SQLAlchemy adds some magic to the columns/fields in a **model class** to make those Python comparisons have super powers. @@ -451,8 +460,11 @@ select(Hero).where(Hero.secret_name == "Pedro Parqueador") I think that alone, having better editor support, autocompletion, and inline errors, is enough to make it worth having expressions instead of keyword arguments. ✨ -!!! tip - **Expressions** also provide more features for other types of comparisons, shown down below. 👇 +/// tip + +**Expressions** also provide more features for other types of comparisons, shown down below. 👇 + +/// ## Exec the Statement @@ -502,12 +514,15 @@ secret_name='Dive Wilson' age=None id=1 name='Deadpond' -!!! tip - The `results` object is an iterable to be used in a `for` loop. +/// tip + +The `results` object is an iterable to be used in a `for` loop. + +Even if we got only one row, we iterate over that `results` object. Just as if it was a list of one element. - Even if we got only one row, we iterate over that `results` object. Just as if it was a list of one element. +We'll see other ways to get the data later. - We'll see other ways to get the data later. +/// ## Other Comparisons @@ -597,8 +612,11 @@ age=36 id=6 name='Dr. Weird' secret_name='Steve Weird' age=93 id=7 name='Captain North America' secret_name='Esteban Rogelios' ``` -!!! tip - Notice that it didn't select `Black Lion`, because the age is not *strictly* greater than `35`. +/// tip + +Notice that it didn't select `Black Lion`, because the age is not *strictly* greater than `35`. + +/// ### More Than or Equal @@ -630,8 +648,11 @@ age=36 id=6 name='Dr. Weird' secret_name='Steve Weird' age=93 id=7 name='Captain North America' secret_name='Esteban Rogelios' ``` -!!! tip - This time we got `Black Lion` too because although the age is not *strictly* greater than `35`it is *equal* to `35`. +/// tip + +This time we got `Black Lion` too because although the age is not *strictly* greater than `35`it is *equal* to `35`. + +/// ### Less Than @@ -660,8 +681,11 @@ And we get the younger one with an age in the database: age=32 id=4 name='Tarantula' secret_name='Natalia Roman-on' ``` -!!! tip - We could imagine that **Spider-Boy** is even **younger**. But because we don't know the age, it is `NULL` in the database (`None` in Python), it doesn't match any of these age comparisons with numbers. +/// tip + +We could imagine that **Spider-Boy** is even **younger**. But because we don't know the age, it is `NULL` in the database (`None` in Python), it doesn't match any of these age comparisons with numbers. + +/// ### Less Than or Equal @@ -691,8 +715,11 @@ age=32 id=4 name='Tarantula' secret_name='Natalia Roman-on' age=35 id=5 name='Black Lion' secret_name='Trevor Challa' ``` -!!! tip - We get `Black Lion` here too because although the age is not *strictly* less than `35` it is *equal* to `35`. +/// tip + +We get `Black Lion` here too because although the age is not *strictly* less than `35` it is *equal* to `35`. + +/// ### Benefits of Expressions @@ -925,10 +952,13 @@ col(Hero.age) > 35 And with that the editor knows this code is actually fine, because this is a special **SQLModel** column. -!!! tip - That `col()` will come handy later, giving autocompletion to several other things we can do with these special **class attributes** for columns. +/// tip + +That `col()` will come handy later, giving autocompletion to several other things we can do with these special **class attributes** for columns. + +But we'll get there later. - But we'll get there later. +/// ## Recap diff --git a/mkdocs.yml b/mkdocs.yml index 646af7c39e..385738823b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -21,6 +21,13 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes + - content.tooltips + - navigation.path + - content.code.annotate + - content.code.copy + - content.code.select + # - navigation.tabs icon: repo: fontawesome/brands/github-alt logo: img/icon-white.svg @@ -91,20 +98,28 @@ nav: - release-notes.md markdown_extensions: +- markdown.extensions.attr_list +- markdown.extensions.tables +- markdown.extensions.md_in_html - toc: permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- admonition -- codehilite -- extra - pymdownx.superfences: custom_fences: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true +- pymdownx.betterem +- pymdownx.highlight +- pymdownx.blocks.details +- pymdownx.blocks.admonition: + types: + - note + - info + - tip + - warning + - danger +- pymdownx.blocks.tab: + alternate_style: True - mdx_include extra: From be464fba69e8f1a38076293d69f3e8c2d58290e2 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 28 Nov 2023 20:50:54 +0000 Subject: [PATCH 304/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index da3f72438f..179bc34695 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update config with new pymdown extensions. PR [#712](https://github.com/tiangolo/sqlmodel/pull/712) by [@tiangolo](https://github.com/tiangolo). * 🙈 Update gitignore, include all coverage files. PR [#711](https://github.com/tiangolo/sqlmodel/pull/711) by [@tiangolo](https://github.com/tiangolo). ### Refactors From 799d0aa7a6025357ebfb499f1c0950593f6443b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 28 Nov 2023 23:12:33 +0100 Subject: [PATCH 305/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20details=20synta?= =?UTF-8?q?x=20with=20new=20pymdown=20extensions=20format=20(#713)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/advanced/decimal.md | 15 ++-- docs/tutorial/automatic-id-none-refresh.md | 40 ++++------ .../tutorial/connect/create-connected-rows.md | 30 +++---- .../connect/create-connected-tables.md | 20 ++--- docs/tutorial/connect/read-connected-data.md | 40 ++++------ .../connect/remove-data-connections.md | 10 +-- .../connect/update-data-connections.md | 10 +-- docs/tutorial/create-db-and-table.md | 45 +++++------ docs/tutorial/delete.md | 40 ++++------ docs/tutorial/fastapi/delete.md | 5 +- docs/tutorial/fastapi/limit-and-offset.md | 5 +- docs/tutorial/fastapi/multiple-models.md | 35 ++++---- docs/tutorial/fastapi/read-one.md | 15 ++-- docs/tutorial/fastapi/relationships.md | 20 ++--- docs/tutorial/fastapi/response-model.md | 10 +-- .../fastapi/session-with-dependency.md | 30 +++---- docs/tutorial/fastapi/simple-hero-api.md | 25 +++--- docs/tutorial/fastapi/teams.md | 20 ++--- docs/tutorial/fastapi/tests.md | 28 +++---- docs/tutorial/fastapi/update.md | 25 +++--- docs/tutorial/indexes.md | 25 +++--- docs/tutorial/insert.md | 45 +++++------ docs/tutorial/limit-and-offset.md | 30 +++---- docs/tutorial/many-to-many/create-data.md | 20 ++--- .../many-to-many/create-models-with-link.md | 25 +++--- .../many-to-many/link-with-extra-fields.md | 30 +++---- .../update-remove-relationships.md | 25 +++--- docs/tutorial/one.md | 55 +++++-------- .../relationship-attributes/back-populates.md | 55 +++++-------- .../create-and-update-relationships.md | 25 +++--- .../define-relationships-attributes.md | 15 ++-- .../read-relationships.md | 20 ++--- .../remove-relationships.md | 10 +-- .../type-annotation-strings.md | 5 +- docs/tutorial/select.md | 45 +++++------ docs/tutorial/update.md | 45 +++++------ docs/tutorial/where.md | 80 ++++++++----------- 37 files changed, 409 insertions(+), 614 deletions(-) diff --git a/docs/advanced/decimal.md b/docs/advanced/decimal.md index 3cd916399f..036aae0003 100644 --- a/docs/advanced/decimal.md +++ b/docs/advanced/decimal.md @@ -47,14 +47,13 @@ Let's say that each hero in the database will have an amount of money. We could # More code here later 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/advanced/decimal/tutorial001.py!} ``` -
+/// Here we are saying that `money` can have at most `5` digits with `max_digits`, **this includes the integers** (to the left of the decimal dot) **and the decimals** (to the right of the decimal dot). @@ -96,14 +95,13 @@ When creating new models you can actually pass normal (`float`) numbers, Pydanti # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/advanced/decimal/tutorial001.py!} ``` -
+/// ## Select Decimal data @@ -117,14 +115,13 @@ Then, when working with Decimal types, you can confirm that they indeed avoid th # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/advanced/decimal/tutorial001.py!} ``` -
+/// ## Review the results diff --git a/docs/tutorial/automatic-id-none-refresh.md b/docs/tutorial/automatic-id-none-refresh.md index e9ac7fdf0a..fdde93a322 100644 --- a/docs/tutorial/automatic-id-none-refresh.md +++ b/docs/tutorial/automatic-id-none-refresh.md @@ -14,14 +14,13 @@ But the same `id` field actually **can be `None`** in the Python code, so we dec # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py!} ``` -
+/// Next, I'll show you a bit more about the synchronization of data between the database and the Python code. @@ -39,14 +38,13 @@ When we create a new `Hero` instance, we don't set the `id`: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py!} ``` -
+/// ### How `Optional` Helps @@ -82,14 +80,13 @@ We can confirm that by printing our heroes before adding them to the database: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py!} ``` -
+/// That will output: @@ -128,14 +125,13 @@ We can verify by creating a session using a `with` block and adding the objects. # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py!} ``` -
+/// This will, again, output the `id`s of the objects as `None`: @@ -168,14 +164,13 @@ Then we can `commit` the changes in the session, and print again: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py!} ``` -
+/// And now, something unexpected happens, look at the output, it seems as if the `Hero` instance objects had no data at all: @@ -241,14 +236,13 @@ To confirm and understand how this **automatic expiration and refresh** of data # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py!} ``` -
+/// Now we are actually accessing the attributes, because instead of printing the whole object `hero_1`: @@ -338,14 +332,13 @@ You can do that too with `session.refresh(object)`: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py!} ``` -
+/// When Python executes this code: @@ -411,14 +404,13 @@ There are no surprises here, it still works: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py!} ``` -
+/// And the output shows again the same data: diff --git a/docs/tutorial/connect/create-connected-rows.md b/docs/tutorial/connect/create-connected-rows.md index 0803432a28..f8845ac772 100644 --- a/docs/tutorial/connect/create-connected-rows.md +++ b/docs/tutorial/connect/create-connected-rows.md @@ -45,14 +45,13 @@ We will later update **Spider-Boy** to add him to the **Preventers** team too, b We will continue with the code in the previous example and we will add more things to it. -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/connect/create_tables/tutorial001.py!} ``` -
+/// Make sure you remove the `database.db` file before running the examples to get the same results. @@ -72,14 +71,13 @@ Let's start by creating two teams: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/connect/insert/tutorial001.py!} ``` -
+/// This would hopefully look already familiar. @@ -103,14 +101,13 @@ Let's not forget to add this function `create_heroes()` to the `main()` function # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/connect/insert/tutorial001.py!} ``` -
+/// ## Run it @@ -151,14 +148,13 @@ As the `Hero` class model now has a field (column, attribute) `team_id`, we can # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/connect/insert/tutorial001.py!} ``` -
+/// We haven't committed this hero to the database yet, but there are already a couple of things to pay **attention** to. @@ -190,14 +186,13 @@ Let's now create two more heroes: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/connect/insert/tutorial001.py!} ``` -
+/// When creating `hero_rusty_man`, we are accessing `team_preventers.id`, so that will also trigger a refresh of its data, generating an output of: @@ -236,14 +231,13 @@ Now let's refresh and print those new heroes to see their new ID pointing to the # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/connect/insert/tutorial001.py!} ``` -
+/// If we execute that in the command line, it will output: diff --git a/docs/tutorial/connect/create-connected-tables.md b/docs/tutorial/connect/create-connected-tables.md index 5500fcc903..411ab3271e 100644 --- a/docs/tutorial/connect/create-connected-tables.md +++ b/docs/tutorial/connect/create-connected-tables.md @@ -63,14 +63,13 @@ Import the things we need from `sqlmodel` and create a new `Team` model: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/connect/create_tables/tutorial001.py!} ``` -
+/// This is very similar to what we have been doing with the `Hero` model. @@ -95,14 +94,13 @@ This is the same model we have been using up to now, we are just adding the new # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/connect/create_tables/tutorial001.py!} ``` -
+/// Most of that should look familiar: @@ -142,14 +140,13 @@ Now we can add the same code as before to create the engine and the function to {!./docs_src/tutorial/connect/create_tables/tutorial001.py[ln:21-28]!} ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/connect/create_tables/tutorial001.py!} ``` -
+/// And as before, we'll call this function from another function `main()`, and we'll add that function `main()` to the main block of the file: @@ -159,14 +156,13 @@ And as before, we'll call this function from another function `main()`, and we'l {!./docs_src/tutorial/connect/create_tables/tutorial001.py[ln:31-36]!} ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/connect/create_tables/tutorial001.py!} ``` -
+/// ## Run the Code diff --git a/docs/tutorial/connect/read-connected-data.md b/docs/tutorial/connect/read-connected-data.md index 91bafc47f2..991d4409c8 100644 --- a/docs/tutorial/connect/read-connected-data.md +++ b/docs/tutorial/connect/read-connected-data.md @@ -35,14 +35,13 @@ And the `hero` table has this data: We will continue with the code in the previous example and we will add more things to it. -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/connect/insert/tutorial001.py!} ``` -
+/// ## `SELECT` Connected Data with SQL @@ -132,14 +131,13 @@ So, we can pass the `Hero` and `Team` model classes. And we can also use both th # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/connect/select/tutorial001.py!} ``` -
+/// Notice that in the comparison with `==` we are using the class attributes for both `Hero.team_id` and `Team.id`. @@ -157,14 +155,13 @@ And as we used `select` with two models, we will receive tuples of instances of # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/connect/select/tutorial001.py!} ``` -
+/// For each iteration in the `for` loop we get a a tuple with an instance of the class `Hero` and an instance of the class `Team`. @@ -190,14 +187,13 @@ As always, we must remember to add this new `select_heroes()` function to the `m # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/connect/select/tutorial001.py!} ``` -
+/// ## Run the Program @@ -312,14 +308,13 @@ And in SQLModel (actually SQLAlchemy), when using the `.join()`, because we alre # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/connect/select/tutorial002.py!} ``` -
+/// Also notice that we are still including `Team` in the `select(Hero, Team)`, because we still want to access that data. @@ -454,14 +449,13 @@ Now let's replicate the same query in **SQLModel**. # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/connect/select/tutorial003.py!} ``` -
+/// And if we run it, it will output: @@ -516,14 +510,13 @@ We could even add some additional `.where()` after `.join()` to filter the data # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/connect/select/tutorial004.py!} ``` -
+/// Here we are **filtering** with `.where()` to get only the heroes that belong to the **Preventers** team. @@ -562,14 +555,13 @@ By putting the `Team` in `select()` we tell **SQLModel** and the database that w # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/connect/select/tutorial005.py!} ``` -
+/// And if we run that, it will output: diff --git a/docs/tutorial/connect/remove-data-connections.md b/docs/tutorial/connect/remove-data-connections.md index 940a09f30d..b220530043 100644 --- a/docs/tutorial/connect/remove-data-connections.md +++ b/docs/tutorial/connect/remove-data-connections.md @@ -35,14 +35,13 @@ Let's see how to **remove** connections between rows in tables. We will continue with the code from the previous chapter. -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/connect/update/tutorial001.py!} ``` -
+/// ## Break a Connection @@ -64,14 +63,13 @@ We can simply set the `team_id` to `None`, and now it doesn't have a connection # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/connect/delete/tutorial001.py!} ``` -
+/// Again, we just **assign** a value to that field attribute `team_id`, now the value is `None`, which means `NULL` in the database. Then we `add()` the hero to the session, and then `commit()`. diff --git a/docs/tutorial/connect/update-data-connections.md b/docs/tutorial/connect/update-data-connections.md index ccc430dd6e..39994c3e91 100644 --- a/docs/tutorial/connect/update-data-connections.md +++ b/docs/tutorial/connect/update-data-connections.md @@ -37,14 +37,13 @@ Now we'll see how to **update** those connections between rows tables. We will continue with the code we used to create some heroes, and we'll update them. -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/connect/insert/tutorial001.py!} ``` -
+/// ## Assign a Team to a Hero @@ -64,14 +63,13 @@ Doing it is just like updating any other field: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/connect/update/tutorial001.py!} ``` -
+/// We can simply **assign** a value to that field attribute `team_id`, then `add()` the hero to the session, and then `commit()`. diff --git a/docs/tutorial/create-db-and-table.md b/docs/tutorial/create-db-and-table.md index 48341b96e0..0d8a9a21ce 100644 --- a/docs/tutorial/create-db-and-table.md +++ b/docs/tutorial/create-db-and-table.md @@ -47,14 +47,13 @@ For that, we will import `SQLModel` (plus other things we will also use) and cre # More code here later 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/create_db_and_table/tutorial001.py!} ``` -
+/// This class `Hero` **represents the table** for our heroes. And each instance we create later will **represent a row** in the table. @@ -82,14 +81,13 @@ And the type of each of them will also be the type of table column: # More code here later 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/create_db_and_table/tutorial001.py!} ``` -
+/// Let's now see with more detail these field/column declarations. @@ -109,14 +107,13 @@ And we also set the default value of `age` to `None`. # More code here later 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/create_db_and_table/tutorial001.py!} ``` -
+/// /// tip @@ -152,14 +149,13 @@ To do that, we use the special `Field` function from `sqlmodel` and set the argu # More code here later 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/create_db_and_table/tutorial001.py!} ``` -
+/// That way, we tell **SQLModel** that this `id` field/column is the primary key of the table. @@ -208,14 +204,13 @@ Creating the **engine** is very simple, just call `create_engine()` with a URL f # More code here later 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/create_db_and_table/tutorial001.py!} ``` -
+/// You should normally have a single **engine** object for your whole application and re-use it everywhere. @@ -245,14 +240,13 @@ SQLite supports a special database that lives all *in memory*. Hence, it's very # More code here later 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/create_db_and_table/tutorial001.py!} ``` -
+/// You can read a lot more about all the databases supported by **SQLAlchemy** (and that way supported by **SQLModel**) in the SQLAlchemy documentation. @@ -270,14 +264,13 @@ It is particularly useful for **learning** and **debugging**: # More code here later 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/create_db_and_table/tutorial001.py!} ``` -
+/// But in production, you would probably want to remove `echo=True`: @@ -416,14 +409,13 @@ Let's run the program to see it all working. Put the code it in a file `app.py` if you haven't already. -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/create_db_and_table/tutorial001.py!} ``` -
+/// /// tip @@ -534,14 +526,13 @@ Let's put it in a function `create_db_and_tables()`: # More code here later 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/create_db_and_table/tutorial002.py!} ``` -
+/// If `SQLModel.metadata.create_all(engine)` was not in a function and we tried to import something from this module (from this file) in another, it would try to create the database and table **every time** we executed that other file that imported this module. diff --git a/docs/tutorial/delete.md b/docs/tutorial/delete.md index 34dd3be423..437d388b87 100644 --- a/docs/tutorial/delete.md +++ b/docs/tutorial/delete.md @@ -6,14 +6,13 @@ Now let's delete some data using **SQLModel**. As before, we'll continue from where we left off with the previous code. -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/update/tutorial003.py!} ``` -
+/// Remember to remove the `database.db` file before running the examples to get the same results. @@ -71,14 +70,13 @@ We'll start by selecting the hero `"Spider-Youngster"` that we updated in the pr # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/delete/tutorial001.py!} ``` -
+/// As this is a new function `delete_heroes()`, we'll also add it to the `main()` function so that we call it when executing the program from the command line: @@ -88,14 +86,13 @@ As this is a new function `delete_heroes()`, we'll also add it to the `main()` f {!./docs_src/tutorial/delete/tutorial001.py[ln:92-100]!} ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/delete/tutorial001.py!} ``` -
+/// That will print the same existing hero **Spider-Youngster**: @@ -131,14 +128,13 @@ Now, very similar to how we used `session.add()` to add or update new heroes, we # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/delete/tutorial001.py!} ``` -
+/// ## Commit the Session @@ -154,14 +150,13 @@ This will save all the changes stored in the **session**, like the deleted hero: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/delete/tutorial001.py!} ``` -
+/// The same as we have seen before, `.commit()` will also save anything else that was added to the session. Including updates, or created heroes. @@ -204,14 +199,13 @@ Because of that, the object still contains its attributes with the data in it, s # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/delete/tutorial001.py!} ``` -
+/// This will output: @@ -242,14 +236,13 @@ To confirm if it was deleted, now let's query the database again, with the same # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/delete/tutorial001.py!} ``` -
+/// Here we are using `results.first()` to get the first object found (in case it found multiple) or `None`, if it didn't find anything. @@ -294,14 +287,13 @@ We'll do it by checking that the "first" item in the `results` is `None`: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/delete/tutorial001.py!} ``` -
+/// This will output: diff --git a/docs/tutorial/fastapi/delete.md b/docs/tutorial/fastapi/delete.md index a48122304b..02fdc9fb52 100644 --- a/docs/tutorial/fastapi/delete.md +++ b/docs/tutorial/fastapi/delete.md @@ -20,14 +20,13 @@ And if we actually find a hero, we just delete it with the **session**. # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/delete/tutorial001.py!} ``` -
+/// After deleting it successfully, we just return a response of: diff --git a/docs/tutorial/fastapi/limit-and-offset.md b/docs/tutorial/fastapi/limit-and-offset.md index 1152101eba..b9d1c8b73c 100644 --- a/docs/tutorial/fastapi/limit-and-offset.md +++ b/docs/tutorial/fastapi/limit-and-offset.md @@ -32,14 +32,13 @@ And by default, we will return a maximum of `100` heroes, so `limit` will have a # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py!} ``` -
+/// We want to allow clients to set different `offset` and `limit` values. diff --git a/docs/tutorial/fastapi/multiple-models.md b/docs/tutorial/fastapi/multiple-models.md index 3833f816a9..d57d1bd9b4 100644 --- a/docs/tutorial/fastapi/multiple-models.md +++ b/docs/tutorial/fastapi/multiple-models.md @@ -119,14 +119,13 @@ The simplest way to solve it could be to create **multiple models**, each one wi # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/multiple_models/tutorial001.py!} ``` -
+/// Here's the important detail, and probably the most important feature of **SQLModel**: only `Hero` is declared with `table = True`. @@ -156,14 +155,13 @@ Let's first check how is the process to create a hero now: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/multiple_models/tutorial001.py!} ``` -
+/// Let's check that in detail. @@ -267,14 +265,13 @@ So let's create a **base** model `HeroBase` that the others can inherit from: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py!} ``` -
+/// As you can see, this is *not* a **table model**, it doesn't have the `table = True` config. @@ -292,14 +289,13 @@ Let's start with the only **table model**, the `Hero`: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py!} ``` -
+/// Notice that `Hero` now doesn't inherit from `SQLModel`, but from `HeroBase`. @@ -323,14 +319,13 @@ Notice that the parent model `HeroBase` is not a **table model**, but still, we # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py!} ``` -
+/// This won't affect this parent **data model** `HeroBase`. @@ -350,14 +345,13 @@ This is a fun one: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py!} ``` -
+/// What's happening here? @@ -385,14 +379,13 @@ This one just declares that the `id` field is required when reading a hero from # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py!} ``` -
+/// ## Review the Updated Docs UI diff --git a/docs/tutorial/fastapi/read-one.md b/docs/tutorial/fastapi/read-one.md index b06ebc2a83..0fab696c19 100644 --- a/docs/tutorial/fastapi/read-one.md +++ b/docs/tutorial/fastapi/read-one.md @@ -22,14 +22,13 @@ If you need to refresh how *path parameters* work, including their data validati {!./docs_src/tutorial/fastapi/read_one/tutorial001.py[ln:61-67]!} ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/read_one/tutorial001.py!} ``` -
+/// For example, to get the hero with ID `2` we would send a `GET` request to: @@ -57,14 +56,13 @@ This will let the client know that they probably made a mistake on their side an {!./docs_src/tutorial/fastapi/read_one/tutorial001.py[ln:61-67]!} ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/read_one/tutorial001.py!} ``` -
+/// ## Return the Hero @@ -80,14 +78,13 @@ And because we are using the `response_model` with `HeroRead`, it will be valida {!./docs_src/tutorial/fastapi/read_one/tutorial001.py[ln:61-67]!} ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/read_one/tutorial001.py!} ``` -
+/// ## Check the Docs UI diff --git a/docs/tutorial/fastapi/relationships.md b/docs/tutorial/fastapi/relationships.md index f152b231c7..81cf4286be 100644 --- a/docs/tutorial/fastapi/relationships.md +++ b/docs/tutorial/fastapi/relationships.md @@ -64,14 +64,13 @@ And the same way, we declared the `TeamRead` with only the same base fields of t # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/teams/tutorial001.py!} ``` -
+/// Now, remember that FastAPI uses the `response_model` to validate and **filter** the response data? @@ -89,14 +88,13 @@ In this case, we used `response_model=TeamRead` and `response_model=HeroRead`, s # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/teams/tutorial001.py!} ``` -
+/// ## Don't Include All the Data @@ -186,14 +184,13 @@ We'll add them **after** the other models so that we can easily reference the pr # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/relationships/tutorial001.py!} ``` -
+/// These two models are very **simple in code**, but there's a lot happening here. Let's check it out. @@ -239,14 +236,13 @@ In the case of the hero, this tells FastAPI to extract the `team` too. And in th # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/relationships/tutorial001.py!} ``` -
+/// ## Check It Out in the Docs UI diff --git a/docs/tutorial/fastapi/response-model.md b/docs/tutorial/fastapi/response-model.md index 29899beaff..f6e20b3354 100644 --- a/docs/tutorial/fastapi/response-model.md +++ b/docs/tutorial/fastapi/response-model.md @@ -40,14 +40,13 @@ For example, we can pass the same `Hero` **SQLModel** class (because it is also # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/response_model/tutorial001.py!} ``` -
+/// ## List of Heroes in `response_model` @@ -65,14 +64,13 @@ First, we import `List` from `typing` and then we declare the `response_model` w # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/response_model/tutorial001.py!} ``` -
+/// ## FastAPI and Response Model diff --git a/docs/tutorial/fastapi/session-with-dependency.md b/docs/tutorial/fastapi/session-with-dependency.md index d4265af9f8..ce2421fa82 100644 --- a/docs/tutorial/fastapi/session-with-dependency.md +++ b/docs/tutorial/fastapi/session-with-dependency.md @@ -14,14 +14,13 @@ Up to now, we have been creating a session in each *path operation*, in a `with` # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/delete/tutorial001.py!} ``` -
+/// That's perfectly fine, but in many use cases we would want to use FastAPI Dependencies, for example to **verify** that the client is **logged in** and get the **current user** before executing any other code in the *path operation*. @@ -43,14 +42,13 @@ It could use `yield` instead of `return`, and in that case **FastAPI** will make # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py!} ``` -
+/// ## Use the Dependency @@ -72,14 +70,13 @@ We import `Depends()` from `fastapi`. Then we use it in the *path operation func # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py!} ``` -
+/// /// tip @@ -121,14 +118,13 @@ This means that in the main code of the *path operation function*, it will work # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py!} ``` -
+/// In fact, you could think that all that block of code inside of the `create_hero()` function is still inside a `with` block for the **session**, because this is more or less what's happening behind the scenes. @@ -148,14 +144,13 @@ But now, the `with` block is not explicitly in the function, but in the dependen # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py!} ``` -
+/// We will see how this is very useful when testing the code later. ✅ @@ -183,14 +178,13 @@ And then we remove the previous `with` block with the old **session**. {!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:55-106]!} ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py!} ``` -
+/// ## Recap diff --git a/docs/tutorial/fastapi/simple-hero-api.md b/docs/tutorial/fastapi/simple-hero-api.md index 8debe579af..2ef8b436d0 100644 --- a/docs/tutorial/fastapi/simple-hero-api.md +++ b/docs/tutorial/fastapi/simple-hero-api.md @@ -43,14 +43,13 @@ This is almost the same code we have seen up to now in previous examples: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py!} ``` -
+/// There's only one change here from the code we have used before, the `check_same_thread` in the `connect_args`. @@ -88,14 +87,13 @@ And then create an `app` object that is an instance of that `FastAPI` class: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py!} ``` -
+/// ## Create Database and Tables on `startup` @@ -111,14 +109,13 @@ This should be called only once at startup, not before every request, so we put # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py!} ``` -
+/// ## Create Heroes *Path Operation* @@ -140,14 +137,13 @@ It will be called when a user sends a request with a `POST` **operation** to the # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py!} ``` -
+/// /// info @@ -187,14 +183,13 @@ Now let's add another **path operation** to read all the heroes: {!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py[ln:25-46]!} ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py!} ``` -
+/// This is pretty straightforward. diff --git a/docs/tutorial/fastapi/teams.md b/docs/tutorial/fastapi/teams.md index 52554cf4b6..0180b9476b 100644 --- a/docs/tutorial/fastapi/teams.md +++ b/docs/tutorial/fastapi/teams.md @@ -24,14 +24,13 @@ And we also create a `TeamUpdate` **data model**. # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/teams/tutorial001.py!} ``` -
+/// We now also have **relationship attributes**. 🎉 @@ -47,14 +46,13 @@ Let's now update the `Hero` models too. # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/teams/tutorial001.py!} ``` -
+/// We now have a `team_id` in the hero models. @@ -74,14 +72,13 @@ Notice that the **relationship attributes**, the ones with `Relationship()`, are # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/teams/tutorial001.py!} ``` -
+/// ## Path Operations for Teams @@ -97,14 +94,13 @@ These are equivalent and very similar to the **path operations** for the **heroe # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/teams/tutorial001.py!} ``` -
+/// ## Using Relationships Attributes diff --git a/docs/tutorial/fastapi/tests.md b/docs/tutorial/fastapi/tests.md index ea084335b4..33b17f87d2 100644 --- a/docs/tutorial/fastapi/tests.md +++ b/docs/tutorial/fastapi/tests.md @@ -14,14 +14,13 @@ We will use the application with the hero models, but without team models, and w Now we will see how useful it is to have this session dependency. ✨ -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/app_testing/tutorial001/main.py!} ``` -
+/// ## File Structure @@ -171,10 +170,8 @@ But **it works great for testing**, because it can be quickly created before eac And also, because it never has to write anything to a file and it's all just in memory, it will be even faster than normally. 🏎 -
- -Other alternatives and ideas 👀 - +/// details | Other alternatives and ideas 👀 + Before arriving at the idea of using an **in-memory database** we could have explored other alternatives and ideas. The first is that we are not deleting the file after we finish the test, so the next test could have **leftover data**. So, the right thing would be to delete the file right after finishing the test. 🔥 @@ -187,7 +184,7 @@ So, if we tried to run the tests at the same time **in parallel** to try to spee Of course, we could also fix that, using some **random name** for each testing database file... but in the case of SQLite, we have an even better alternative by just using an **in-memory database**. ✨ -
+/// ## Configure the In-Memory Database @@ -315,14 +312,13 @@ Let's add some more tests: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main.py!} ``` -
+/// /// tip @@ -352,14 +348,13 @@ But for the next test function, we will require **both fixtures**, the **client* # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main.py!} ``` -
+/// In this test function, we want to check that the *path operation* to **read a list of heroes** actually sends us heroes. @@ -391,14 +386,13 @@ Using the same ideas, requiring the fixtures, creating data that we need for the {!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main.py[ln:84-125]!} ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main.py!} ``` -
+/// ## Run the Tests diff --git a/docs/tutorial/fastapi/update.md b/docs/tutorial/fastapi/update.md index df04600510..27c413f387 100644 --- a/docs/tutorial/fastapi/update.md +++ b/docs/tutorial/fastapi/update.md @@ -30,14 +30,13 @@ So, let's create this new `HeroUpdate` model: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/update/tutorial001.py!} ``` -
+/// This is almost the same as `HeroBase`, but all the fields are optional, so we can't simply inherit from `HeroBase`. @@ -55,14 +54,13 @@ We will use a `PATCH` HTTP operation. This is used to **partially update data**, # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/update/tutorial001.py!} ``` -
+/// We also read the `hero_id` from the *path parameter* and the request body, a `HeroUpdate`. @@ -80,14 +78,13 @@ So, we need to read the hero from the database, with the **same logic** we used # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/update/tutorial001.py!} ``` -
+/// ### Get the New Data @@ -147,14 +144,13 @@ Then we use that to get the data that was actually sent by the client: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/update/tutorial001.py!} ``` -
+/// ## Update the Hero in the Database @@ -168,14 +164,13 @@ Now that we have a **dictionary with the data sent by the client**, we can itera # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/fastapi/update/tutorial001.py!} ``` -
+/// If you are not familiar with that `setattr()`, it takes an object, like the `db_hero`, then an attribute name (`key`), that in our case could be `"name"`, and a value (`value`). And then it **sets the attribute with that name to the value**. diff --git a/docs/tutorial/indexes.md b/docs/tutorial/indexes.md index 9a4be7dbfb..a7c6028e88 100644 --- a/docs/tutorial/indexes.md +++ b/docs/tutorial/indexes.md @@ -20,14 +20,13 @@ Are you already a **SQL expert** and don't have time for all my explanations? Fine, in that case, you can **sneak peek** the final code to create indexes here. -
-👀 Full file preview +/// details | 👀 Full file preview ```Python hl_lines="8 10" {!./docs_src/tutorial/indexes/tutorial002.py!} ``` -
+/// ..but if you are not an expert, **continue reading**, this will probably be useful. 🤓 @@ -270,14 +269,13 @@ Here's the `Hero` model we had before: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/where/tutorial001.py!} ``` -
+/// Let's now update it to tell **SQLModel** to create an index for the `name` field when creating the table: @@ -287,14 +285,13 @@ Let's now update it to tell **SQLModel** to create an index for the `name` field # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/indexes/tutorial001.py!} ``` -
+/// We use the same `Field()` again as we did before, and set `index=True`. That's it! 🚀 @@ -324,14 +321,13 @@ This is great because it means that indexes are very **simple to use**. But it m # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/indexes/tutorial001.py!} ``` -
+/// This is exactly the same code as we had before, but now the database will **use the index** underneath. @@ -380,14 +376,13 @@ We are going to query the `hero` table doing comparisons on the `age` field too, # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/indexes/tutorial002.py!} ``` -
+/// In this case, we want the default value of `age` to continue being `None`, so we set `default=None` when using `Field()`. diff --git a/docs/tutorial/insert.md b/docs/tutorial/insert.md index 31b387dced..9b01db339e 100644 --- a/docs/tutorial/insert.md +++ b/docs/tutorial/insert.md @@ -135,14 +135,13 @@ We'll create 3 right away, for the 3 heroes: # More code here later 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/insert/tutorial002.py!} ``` -
+/// /// tip @@ -180,14 +179,13 @@ The first step is to import the `Session` class: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/insert/tutorial001.py!} ``` -
+/// Then we can create a new session: @@ -199,14 +197,13 @@ Then we can create a new session: # More code here later 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/insert/tutorial001.py!} ``` -
+/// The new `Session` takes an `engine` as a parameter. And it will use the **engine** underneath. @@ -227,14 +224,13 @@ Now that we have some hero model instances (some objects in memory) and a **sess # More code here later 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/insert/tutorial001.py!} ``` -
+/// By this point, our heroes are *not* stored in the database yet. @@ -265,14 +261,13 @@ Now that we have the heroes in the **session** and that we are ready to save all # More code here later 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/insert/tutorial001.py!} ``` -
+/// Once this line is executed, the **session** will use the **engine** to save all the data in the database by sending the corresponding SQL. @@ -306,14 +301,13 @@ But to keep things a bit more organized, let's instead create a new function `ma # More code here later 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/insert/tutorial002.py!} ``` -
+/// And then we can call that single `main()` function from that main block: @@ -322,14 +316,13 @@ And then we can call that single `main()` function from that main block: {!./docs_src/tutorial/insert/tutorial002.py[ln:36-42]!} ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/insert/tutorial002.py!} ``` -
+/// By having everything that should happen when called as a script in a single function, we can easily add more code later on. @@ -392,14 +385,13 @@ So once we are done with the session, we should **close** it to make it release # More code here later 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/insert/tutorial001.py!} ``` -
+/// But what happens if we forget to close the session? @@ -418,14 +410,13 @@ But there's a better way to handle the session, using a `with` block: {!./docs_src/tutorial/insert/tutorial002.py[ln:23-33]!} ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/insert/tutorial002.py!} ``` -
+/// This is the same as creating the session manually and then manually closing it. But here, using a `with` block, it will be automatically created when **starting** the `with` block and assigned to the variable `session`, and it will be automatically closed after the `with` block is **finished**. diff --git a/docs/tutorial/limit-and-offset.md b/docs/tutorial/limit-and-offset.md index bcd17b2f8f..b3fd1514a2 100644 --- a/docs/tutorial/limit-and-offset.md +++ b/docs/tutorial/limit-and-offset.md @@ -22,14 +22,13 @@ Again, we will create several heroes to have some data to select from: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/offset_and_limit/tutorial001.py!} ``` -
+/// ## Review Select All @@ -43,14 +42,13 @@ This is the code we had to select all the heroes in the `select()` examples: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/select/tutorial003.py!} ``` -
+/// But this would get us **all** the heroes at the same time, in a database that could have thousands, that could be problematic. @@ -66,14 +64,13 @@ We currently have 7 heroes in the database. But we could as well have thousands, # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/offset_and_limit/tutorial001.py!} ``` -
+/// The special **select** object we get from `select()` also has a method `.limit()` that we can use to limit the results to a certain number. @@ -144,14 +141,13 @@ We can use `.offset()`: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/offset_and_limit/tutorial002.py!} ``` -
+/// The way this works is that the special **select** object we get from `select()` has methods like `.where()`, `.offset()` and `.limit()`. @@ -198,14 +194,13 @@ Then to get the next batch of 3 rows we would offset all the ones we already saw # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/offset_and_limit/tutorial003.py!} ``` -
+/// The database right now has **only 7 rows**, so this query can only get 1 row. @@ -268,14 +263,13 @@ Of course, you can also combine `.limit()` and `.offset()` with `.where()` and o # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/offset_and_limit/tutorial004.py!} ``` -
+/// ## Run the Program with Limit, Offset, and Where on the Command Line diff --git a/docs/tutorial/many-to-many/create-data.md b/docs/tutorial/many-to-many/create-data.md index 971659b9a6..3a7719c960 100644 --- a/docs/tutorial/many-to-many/create-data.md +++ b/docs/tutorial/many-to-many/create-data.md @@ -8,14 +8,13 @@ We'll create data for this same **many-to-many** relationship with a link table: We'll continue from where we left off with the previous code. -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/many_to_many/tutorial001.py!} ``` -
+/// ## Create Heroes @@ -29,14 +28,13 @@ As we have done before, we'll create a function `create_heroes()` and we'll crea # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/many_to_many/tutorial001.py!} ``` -
+/// This is very similar to what we have done before. @@ -58,14 +56,13 @@ Now let's do as we have done before, `commit` the **session**, `refresh` the dat # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/many_to_many/tutorial001.py!} ``` -
+/// ## Add to Main @@ -79,14 +76,13 @@ As before, add the `create_heroes()` function to the `main()` function to make s # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/many_to_many/tutorial001.py!} ``` -
+/// ## Run the Program diff --git a/docs/tutorial/many-to-many/create-models-with-link.md b/docs/tutorial/many-to-many/create-models-with-link.md index 63cbf3eb82..8ad06d84b6 100644 --- a/docs/tutorial/many-to-many/create-models-with-link.md +++ b/docs/tutorial/many-to-many/create-models-with-link.md @@ -18,14 +18,13 @@ We can create it just as any other **SQLModel**: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/many_to_many/tutorial001.py!} ``` -
+/// This is a **SQLModel** class model table like any other. @@ -47,14 +46,13 @@ Let's see the `Team` model, it's almost identical as before, but with a little c # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/many_to_many/tutorial001.py!} ``` -
+/// The **relationship attribute `heroes`** is still a list of heroes, annotated as `List["Hero"]`. Again, we use `"Hero"` in quotes because we haven't declared that class yet by this point in the code (but as you know, editors and **SQLModel** understand that). @@ -76,14 +74,13 @@ Let's see the other side, here's the `Hero` model: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/many_to_many/tutorial001.py!} ``` -
+/// We **removed** the previous `team_id` field (column) because now the relationship is done via the link table. 🔥 @@ -109,14 +106,13 @@ The same as before, we will have the rest of the code to create the **engine**, # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/many_to_many/tutorial001.py!} ``` -
+/// And as in previous examples, we will add that function to a function `main()`, and we will call that `main()` function in the main block: @@ -130,14 +126,13 @@ And as in previous examples, we will add that function to a function `main()`, a {!./docs_src/tutorial/many_to_many/tutorial001.py[ln:83-84]!} ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/many_to_many/tutorial001.py!} ``` -
+/// ## Run the Code diff --git a/docs/tutorial/many-to-many/link-with-extra-fields.md b/docs/tutorial/many-to-many/link-with-extra-fields.md index 90839c61c6..bda78d1ec3 100644 --- a/docs/tutorial/many-to-many/link-with-extra-fields.md +++ b/docs/tutorial/many-to-many/link-with-extra-fields.md @@ -40,14 +40,13 @@ And we will also add two **relationship attributes**, for the linked `team` and # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/many_to_many/tutorial003.py!} ``` -
+/// The new **relationship attributes** have their own `back_populates` pointing to new relationship attributes we will create in the `Hero` and `Team` models: @@ -76,14 +75,13 @@ We no longer have the `heroes` relationship attribute, and instead we have the n # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/many_to_many/tutorial003.py!} ``` -
+/// ## Update Hero Model @@ -99,14 +97,13 @@ We change the `teams` relationship attribute for `team_links`: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/many_to_many/tutorial003.py!} ``` -
+/// ## Create Relationships @@ -122,14 +119,13 @@ But now we create the **explicit link models** manually, pointing to their hero # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/many_to_many/tutorial003.py!} ``` -
+/// We are just adding the link model instances to the session, because the link model instances are connected to the heroes and teams, they will be also automatically included in the session when we commit. @@ -235,14 +231,13 @@ Here we do that in the `update_heroes()` function: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/many_to_many/tutorial003.py!} ``` -
+/// ## Run the Program with the New Relationship @@ -335,14 +330,13 @@ We can do that by iterating on the links: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/many_to_many/tutorial003.py!} ``` -
+/// ## Run the Program with the Updated Relationships diff --git a/docs/tutorial/many-to-many/update-remove-relationships.md b/docs/tutorial/many-to-many/update-remove-relationships.md index f051c3c190..ff4f5dd096 100644 --- a/docs/tutorial/many-to-many/update-remove-relationships.md +++ b/docs/tutorial/many-to-many/update-remove-relationships.md @@ -4,14 +4,13 @@ Now we'll see how to update and remove these **many-to-many** relationships. We'll continue from where we left off with the previous code. -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/many_to_many/tutorial001.py!} ``` -
+/// ## Get Data to Update @@ -33,14 +32,13 @@ And because we are now using `select()`, we also have to import it. # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/many_to_many/tutorial002.py!} ``` -
+/// And of course, we have to add `update_heroes()` to our `main()` function: @@ -50,14 +48,13 @@ And of course, we have to add `update_heroes()` to our `main()` function: {!./docs_src/tutorial/many_to_many/tutorial002.py[ln:100-107]!} ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/many_to_many/tutorial002.py!} ``` -
+/// ## Add Many-to-Many Relationships @@ -73,14 +70,13 @@ We can use the same **relationship attributes** to include `hero_spider_boy` in # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/many_to_many/tutorial002.py!} ``` -
+/// /// tip @@ -173,14 +169,13 @@ In this case, we use the method `.remove()`, that takes an item and removes it f # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/many_to_many/tutorial002.py!} ``` -
+/// And this time, just to show again that by using `back_populates` **SQLModel** (actually SQLAlchemy) takes care of connecting the models by their relationships, even though we performed the operation from the `hero_spider_boy` object (modifying `hero_spider_boy.teams`), we are adding `team_z_force` to the **session**. And we commit that, without even add `hero_spider_boy`. diff --git a/docs/tutorial/one.md b/docs/tutorial/one.md index 6419d9cd71..75a4cc5c7b 100644 --- a/docs/tutorial/one.md +++ b/docs/tutorial/one.md @@ -14,14 +14,13 @@ Let's see the utilities to read a single row. We'll continue with the same examples we have been using in the previous chapters to create and select data and we'll keep updating them. -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/indexes/tutorial002.py!} ``` -
+/// If you already executed the previous examples and have a database with data, **remove the database file** before running each example, that way you won't have duplicate data and you will be able to get the same results. @@ -37,14 +36,13 @@ We have been iterating over the rows in a `result` object like: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/indexes/tutorial002.py!} ``` -
+/// But let's say that we are not interested in all the rows, just the **first** one. @@ -58,14 +56,13 @@ We can call the `.first()` method on the `results` object to get the first row: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/one/tutorial001.py!} ``` -
+/// This will return the first object in the `results` (if there was any). @@ -114,14 +111,13 @@ In that case, `.first()` will return `None`: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/one/tutorial002.py!} ``` -
+/// In this case, as there's no hero with an age less than 25, `.first()` will return `None`. @@ -162,14 +158,13 @@ In that case, instead of `.first()` we can use `.one()`: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/one/tutorial003.py!} ``` -
+/// Here we know that there's only one `"Deadpond"`, and there shouldn't be any more than one. @@ -233,14 +228,13 @@ Of course, even if we don't duplicate the data, we could get the same error if w # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/one/tutorial004.py!} ``` -
+/// That would find 2 rows, and would end up with the same error. @@ -256,14 +250,13 @@ And also, if we get no rows at all with `.one()`, it will also raise an error: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/one/tutorial005.py!} ``` -
+/// In this case, as there are no heroes with an age less than 25, `.one()` will raise an error. @@ -304,14 +297,13 @@ Of course, with `.first()` and `.one()` you would also probably write all that i # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/one/tutorial006.py!} ``` -
+/// That would result in the same as some examples above. @@ -329,14 +321,13 @@ You could do it the same way we have been doing with a `.where()` and then getti # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/one/tutorial007.py!} ``` -
+/// That would work correctly, as expected. But there's a shorter version. 👇 @@ -352,14 +343,13 @@ As selecting a single row by its Id column with the **primary key** is a common # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/one/tutorial008.py!} ``` -
+/// `session.get(Hero, 1)` is an equivalent to creating a `select()`, then filtering by Id using `.where()`, and then getting the first item with `.first()`. @@ -396,14 +386,13 @@ Hero: secret_name='Dive Wilson' age=None id=1 name='Deadpond' # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/one/tutorial009.py!} ``` -
+/// Running that will output: diff --git a/docs/tutorial/relationship-attributes/back-populates.md b/docs/tutorial/relationship-attributes/back-populates.md index e56bcc2460..0c48bd790a 100644 --- a/docs/tutorial/relationship-attributes/back-populates.md +++ b/docs/tutorial/relationship-attributes/back-populates.md @@ -26,14 +26,13 @@ Let's see how that works by writing an **incomplete** version first, without `ba # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py!} ``` -
+/// ## Read Data Objects @@ -49,14 +48,13 @@ As you already know how this works, I won't separate that in a select `statement # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py!} ``` -
+/// /// tip @@ -76,14 +74,13 @@ Now, let's print the current **Spider-Boy**, the current **Preventers** team, an # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py!} ``` -
+/// Up to this point, it's all good. 😊 @@ -117,14 +114,13 @@ Now let's update **Spider-Boy**, removing him from the team by setting `hero_spi # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py!} ``` -
+/// The first important thing is, we *haven't committed* the hero yet, so accessing the list of heroes would not trigger an automatic refresh. @@ -176,14 +172,13 @@ Now, if we commit it and print again: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py!} ``` -
+/// When we access `preventers_team.heroes` after the `commit`, that triggers a refresh, so we get the latest list, without **Spider-Boy**, so that's fine again: @@ -221,14 +216,13 @@ Let's add it back: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py!} ``` -
+/// And we can keep the rest of the code the same: @@ -244,14 +238,13 @@ And we can keep the rest of the code the same: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py!} ``` -
+/// /// tip @@ -290,14 +283,13 @@ It's quite simple code, it's just a string, but it might be confusing to think e # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py!} ``` -
+/// The string in `back_populates` is the name of the attribute *in the other* model, that will reference *the current* model. @@ -313,14 +305,13 @@ So, in the class `Team`, we have an attribute `heroes` and we declare it with `R # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py!} ``` -
+/// The string in `back_populates="team"` refers to the attribute `team` in the class `Hero` (the other class). @@ -336,14 +327,13 @@ So, the string `"heroes"` refers to the attribute `heroes` in the class `Team`. # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py!} ``` -
+/// /// tip @@ -376,11 +366,10 @@ So, `back_populates` would most probably be something like `"hero"` or `"heroes" # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py!} ``` -
+/// diff --git a/docs/tutorial/relationship-attributes/create-and-update-relationships.md b/docs/tutorial/relationship-attributes/create-and-update-relationships.md index dc05af6eb6..e939141193 100644 --- a/docs/tutorial/relationship-attributes/create-and-update-relationships.md +++ b/docs/tutorial/relationship-attributes/create-and-update-relationships.md @@ -14,14 +14,13 @@ Let's check the old code we used to create some heroes and teams: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/connect/insert/tutorial001.py!} ``` -
+/// There are several things to **notice** here. @@ -49,14 +48,13 @@ Now let's do all that, but this time using the new, shiny `Relationship` attribu # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py!} ``` -
+/// Now we can create the `Team` instances and pass them directly to the new `team` argument when creating the `Hero` instances, as `team=team_preventers` instead of `team_id=team_preventers.id`. @@ -84,14 +82,13 @@ The same way we could assign an integer with a `team.id` to a `hero.team_id`, we # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py!} ``` -
+/// ## Create a Team with Heroes @@ -111,14 +108,13 @@ We could also create the `Hero` instances first, and then pass them in the `hero # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py!} ``` -
+/// Here we create two heroes first, **Black Lion** and **Princess Sure-E**, and then we pass them in the `heroes` argument. @@ -146,14 +142,13 @@ Let's create some more heroes and add them to the `team_preventers.heroes` list # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py!} ``` -
+/// The attribute `team_preventers.heroes` behaves like a list. But it's a special type of list, because when we modify it adding heroes to it, **SQLModel** (actually SQLAlchemy) **keeps track of the necessary changes** to be done in the database. diff --git a/docs/tutorial/relationship-attributes/define-relationships-attributes.md b/docs/tutorial/relationship-attributes/define-relationships-attributes.md index 0c2b4f1ef7..7585480923 100644 --- a/docs/tutorial/relationship-attributes/define-relationships-attributes.md +++ b/docs/tutorial/relationship-attributes/define-relationships-attributes.md @@ -47,14 +47,13 @@ Up to now, we have only used the `team_id` column to connect the tables when que # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/connect/insert/tutorial001.py!} ``` -
+/// This is a **plain field** like all the others, all representing a **column in the table**. @@ -68,14 +67,13 @@ First, import `Relationship` from `sqlmodel`: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py!} ``` -
+/// Next, use that `Relationship` to declare a new attribute in the model classes: @@ -85,14 +83,13 @@ Next, use that `Relationship` to declare a new attribute in the model classes: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py!} ``` -
+/// ## What Are These Relationship Attributes diff --git a/docs/tutorial/relationship-attributes/read-relationships.md b/docs/tutorial/relationship-attributes/read-relationships.md index 3a281a2038..e4a049d2bd 100644 --- a/docs/tutorial/relationship-attributes/read-relationships.md +++ b/docs/tutorial/relationship-attributes/read-relationships.md @@ -18,14 +18,13 @@ First, add a function `select_heroes()` where we get a hero to start working wit # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py!} ``` -
+/// ## Select the Related Team - Old Way @@ -41,14 +40,13 @@ With what we have learned **up to now**, we could use a `select()` statement, th # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py!} ``` -
+/// ## Get Relationship Team - New Way @@ -68,14 +66,13 @@ So, the highlighted block above, has the same results as the block below: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py!} ``` -
+/// /// tip @@ -97,14 +94,13 @@ And the same way, when we are working on the **many** side of the **one-to-many* # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py!} ``` -
+/// That would print a list with all the heroes in the Preventers team: diff --git a/docs/tutorial/relationship-attributes/remove-relationships.md b/docs/tutorial/relationship-attributes/remove-relationships.md index ae4c6cfea2..982e316c2b 100644 --- a/docs/tutorial/relationship-attributes/remove-relationships.md +++ b/docs/tutorial/relationship-attributes/remove-relationships.md @@ -16,14 +16,13 @@ We can remove the relationship by setting it to `None`, the same as with the `te # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py!} ``` -
+/// And of course, we should remember to add this `update_heroes()` function to `main()` so that it runs when we call this program from the command line: @@ -35,14 +34,13 @@ And of course, we should remember to add this `update_heroes()` function to `mai # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py!} ``` -
+/// ## Recap diff --git a/docs/tutorial/relationship-attributes/type-annotation-strings.md b/docs/tutorial/relationship-attributes/type-annotation-strings.md index e51e71c106..780da6a962 100644 --- a/docs/tutorial/relationship-attributes/type-annotation-strings.md +++ b/docs/tutorial/relationship-attributes/type-annotation-strings.md @@ -8,14 +8,13 @@ In the first Relationship attribute, we declare it with `List["Hero"]`, putting # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py!} ``` -
+/// What's that about? Can't we just write it normally as `List[Hero]`? diff --git a/docs/tutorial/select.md b/docs/tutorial/select.md index 43003231c9..5be5e8a0ba 100644 --- a/docs/tutorial/select.md +++ b/docs/tutorial/select.md @@ -23,14 +23,13 @@ Things are getting more exciting! Let's now see how to read data from the databa Let's continue from the last code we used to create some data. -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/insert/tutorial002.py!} ``` -
+/// We are creating a **SQLModel** `Hero` class model and creating some records. @@ -175,14 +174,13 @@ We will start with that in a new function `select_heroes()`: # More code here later 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/select/tutorial001.py!} ``` -
+/// ## Create a `select` Statement @@ -196,14 +194,13 @@ First we have to import `select` from `sqlmodel` at the top of the file: # More code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/select/tutorial001.py!} ``` -
+/// And then we will use it to create a `SELECT` statement in Python code: @@ -217,14 +214,13 @@ And then we will use it to create a `SELECT` statement in Python code: # More code here later 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/select/tutorial001.py!} ``` -
+/// It's a very simple line of code that conveys a lot of information: @@ -263,14 +259,13 @@ Now that we have the `select` statement, we can execute it with the **session**: # More code here later 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/select/tutorial001.py!} ``` -
+/// This will tell the **session** to go ahead and use the **engine** to execute that `SELECT` statement in the database and bring the results back. @@ -316,14 +311,13 @@ Now we can put it in a `for` loop and print each one of the heroes: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/select/tutorial001.py!} ``` -
+/// This will print the output: @@ -345,14 +339,13 @@ Now include a call to `select_heroes()` in the `main()` function so that it is e # More code here later 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/select/tutorial001.py!} ``` -
+/// ## Review The Code @@ -406,14 +399,13 @@ The special `results` object also has a method `results.all()` that returns a li # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/select/tutorial003.py!} ``` -
+/// With this now we have all the heroes in a list in the `heroes` variable. @@ -447,14 +439,13 @@ But knowing what is each object and what it is all doing, we can simplify it a b # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/select/tutorial004.py!} ``` -
+/// Here we are putting it all on a single line, you will probably put the select statements in a single line like this more often. diff --git a/docs/tutorial/update.md b/docs/tutorial/update.md index 47ee881adc..b223d6d279 100644 --- a/docs/tutorial/update.md +++ b/docs/tutorial/update.md @@ -6,14 +6,13 @@ Now let's see how to update data using **SQLModel**. As before, we'll continue from where we left off with the previous code. -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/indexes/tutorial002.py!} ``` -
+/// Remember to remove the `database.db` file before running the examples to get the same results. @@ -102,14 +101,13 @@ We'll start by selecting the hero `"Spider-Boy"`, this is the one we will update # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/update/tutorial001.py!} ``` -
+/// Let's not forget to add that `update_heroes()` function to the `main()` function so that we call it when executing the program from the command line: @@ -119,14 +117,13 @@ Let's not forget to add that `update_heroes()` function to the `main()` function {!./docs_src/tutorial/update/tutorial001.py[ln:58-65]!} ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/update/tutorial001.py!} ``` -
+/// Up to that point, running that in the command line will output: @@ -169,14 +166,13 @@ In this case, we will set the `age` to `16`: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/update/tutorial001.py!} ``` -
+/// ## Add the Hero to the Session @@ -192,14 +188,13 @@ This is the same we did when creating new hero instances: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/update/tutorial001.py!} ``` -
+/// ## Commit the Session @@ -215,14 +210,13 @@ This will save the updated hero in the database: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/update/tutorial001.py!} ``` -
+/// It will also save anything else that was added to the session. @@ -263,14 +257,13 @@ But in this example we are not accessing any attribute, we will only print the o # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/update/tutorial001.py!} ``` -
+/// This refresh will trigger the same SQL query that would be automatically triggered by accessing an attribute. So it will generate this output: @@ -304,14 +297,13 @@ Now we can just print the hero: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/update/tutorial001.py!} ``` -
+/// Because we refreshed it right after updating it, it has fresh data, including the new `age` we just updated. @@ -364,14 +356,13 @@ This also means that you can update several fields (attributes, columns) at once {!./docs_src/tutorial/update/annotations/en/tutorial004.md!} -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/update/tutorial004.py!} ``` -
+/// /// tip diff --git a/docs/tutorial/where.md b/docs/tutorial/where.md index 541b0833bc..25c01e9290 100644 --- a/docs/tutorial/where.md +++ b/docs/tutorial/where.md @@ -31,14 +31,13 @@ We'll continue with the same examples we have been using in the previous chapter And now we will update `select_heroes()` to filter the data. -
-👀 Full file preview +/// details | 👀 Full file preview ```Python hl_lines="36-41" {!./docs_src/tutorial/select/tutorial001.py!} ``` -
+/// If you already executed the previous examples and have a database with data, **remove the database file** before running each example, that way you won't have duplicate data and you will be able to get the same results. @@ -198,14 +197,13 @@ We care specially about the **select** statement: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/select/tutorial001.py!} ``` -
+/// ## Filter Rows Using `WHERE` with **SQLModel** @@ -219,14 +217,13 @@ Now, the same way that we add `WHERE` to a SQL statement to filter rows, we can # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/where/tutorial001.py!} ``` -
+/// It's a very small change, but it's packed of details. Let's explore them. @@ -480,14 +477,13 @@ It's actually the same as in previous chapters for selecting data: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/where/tutorial001.py!} ``` -
+/// We take that statement, that now includes a `WHERE`, and we `exec()` it to get the results. @@ -544,14 +540,13 @@ We could get the rows where a column is **not** equal to a value using `!=`: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/where/tutorial002.py!} ``` -
+/// That would output: @@ -572,14 +567,13 @@ Let's update the function `create_heroes()` and add some more rows to make the n # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/where/tutorial003.py!} ``` -
+/// Now that we have several heroes with different ages, it's gonna be more obvious what the next comparisons do. @@ -595,14 +589,13 @@ Now let's use `>` to get the rows where a column is **more than** a value: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/where/tutorial003.py!} ``` -
+/// That would output: @@ -630,14 +623,13 @@ Let's do that again, but with `>=` to get the rows where a column is **more than # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/where/tutorial004.py!} ``` -
+/// Because we are using `>=`, the age `35` will be included in the output: @@ -666,14 +658,13 @@ Similarly, we can use `<` to get the rows where a column is **less than** a valu # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/where/tutorial005.py!} ``` -
+/// And we get the younger one with an age in the database: @@ -699,14 +690,13 @@ Finally, we can use `<=` to get the rows where a column is **less than or equal* # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/where/tutorial006.py!} ``` -
+/// And we get the younger ones, `35` and below: @@ -739,14 +729,13 @@ Because `.where()` returns the same special select object back, we can add more # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/where/tutorial007.py!} ``` -
+/// This will select the rows `WHERE` the `age` is **greater than or equal** to `35`, `AND` also the `age` is **less than** `40`. @@ -795,14 +784,13 @@ As an alternative to using multiple `.where()` we can also pass several expressi # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/where/tutorial008.py!} ``` -
+/// This is the same as the above, and will result in the same output with the two heroes: @@ -825,14 +813,13 @@ To do it, you can import `or_`: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/where/tutorial009.py!} ``` -
+/// And then pass both expressions to `or_()` and put it inside `.where()`. @@ -846,14 +833,13 @@ For example, here we select the heroes that are the youngest OR the oldest: # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/where/tutorial009.py!} ``` -
+/// When we run it, this generates the output: @@ -910,14 +896,13 @@ To do that, we can import `col()` (as short for "column"): # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/where/tutorial011.py!} ``` -
+/// And then put the **class attribute** inside `col()` when using it in a `.where()`: @@ -929,14 +914,13 @@ And then put the **class attribute** inside `col()` when using it in a `.where() # Code below omitted 👇 ``` -
-👀 Full file preview +/// details | 👀 Full file preview ```Python {!./docs_src/tutorial/where/tutorial011.py!} ``` -
+/// So, now the comparison is not: From 2b0dfb50c81247e3e149765abdb92635aef35d17 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 28 Nov 2023 22:12:55 +0000 Subject: [PATCH 306/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 179bc34695..aef3b8abb6 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -13,6 +13,10 @@ * ⬆️ Add support for Python 3.11 and Python 3.12. PR [#710](https://github.com/tiangolo/sqlmodel/pull/710) by [@tiangolo](https://github.com/tiangolo). +### Internal + +* 📝 Update details syntax with new pymdown extensions format. PR [#713](https://github.com/tiangolo/sqlmodel/pull/713) by [@tiangolo](https://github.com/tiangolo). + ## 0.0.12 ### Features From 33c5e5c98d328cc9f2e3597cf8e654ea45464104 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 28 Nov 2023 23:41:03 +0100 Subject: [PATCH 307/906] =?UTF-8?q?=F0=9F=94=A7=20Show=20line=20numbers=20?= =?UTF-8?q?in=20docs=20during=20local=20development=20(#714)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🔧 Show line numbers during local development --- mkdocs.yml | 3 ++- scripts/docs-live.sh | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 385738823b..a41839c12f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -109,7 +109,8 @@ markdown_extensions: class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' - pymdownx.betterem -- pymdownx.highlight +- pymdownx.highlight: + linenums: !ENV [LINENUMS, false] - pymdownx.blocks.details - pymdownx.blocks.admonition: types: diff --git a/scripts/docs-live.sh b/scripts/docs-live.sh index f9d31ba361..b6071fdb97 100755 --- a/scripts/docs-live.sh +++ b/scripts/docs-live.sh @@ -4,4 +4,4 @@ set -e export DYLD_FALLBACK_LIBRARY_PATH="/opt/homebrew/lib" -mkdocs serve --dev-addr 127.0.0.1:8008 +LINENUMS="true" mkdocs serve --dev-addr 127.0.0.1:8008 From cce30d7546a3716d5da27c64188324f807f99680 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 28 Nov 2023 22:41:22 +0000 Subject: [PATCH 308/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index aef3b8abb6..b2661a3578 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -15,6 +15,7 @@ ### Internal +* 🔧 Show line numbers in docs during local development. PR [#714](https://github.com/tiangolo/sqlmodel/pull/714) by [@tiangolo](https://github.com/tiangolo). * 📝 Update details syntax with new pymdown extensions format. PR [#713](https://github.com/tiangolo/sqlmodel/pull/713) by [@tiangolo](https://github.com/tiangolo). ## 0.0.12 From d8effcbc5cef783063d59e3cc3dd6e7c492ca4c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 29 Nov 2023 16:51:55 +0100 Subject: [PATCH 309/906] =?UTF-8?q?=F0=9F=93=9D=20Add=20source=20examples?= =?UTF-8?q?=20for=20Python=203.9=20and=203.10=20(#715)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📝 Add source examples for Python 3.9 and 3.10 * ✅ Add tests for new source examples for Python 3.9 and 3.10, still needs pytest markers * ✅ Add tests for fastapi examples * ✅ Update tests for FastAPI app testing, for Python 3.9 and 3.10, fixing multi-app testing conflicts * ✅ Require Python 3.9 and 3.10 for tests * ✅ Update tests with missing markers --- .../advanced/decimal/tutorial001_py310.py | 59 ++ .../tutorial001_py310.py | 79 +++ .../tutorial002_py310.py | 80 +++ .../tutorial001_py310/__init__.py | 0 .../code_structure/tutorial001_py310/app.py | 29 + .../tutorial001_py310/database.py | 10 + .../tutorial001_py310/models.py | 19 + .../tutorial001_py39/__init__.py | 0 .../code_structure/tutorial001_py39/app.py | 29 + .../tutorial001_py39/database.py | 10 + .../code_structure/tutorial001_py39/models.py | 21 + .../tutorial002_py310/__init__.py | 0 .../code_structure/tutorial002_py310/app.py | 30 + .../tutorial002_py310/database.py | 10 + .../tutorial002_py310/hero_model.py | 16 + .../tutorial002_py310/team_model.py | 14 + .../tutorial002_py39/__init__.py | 0 .../code_structure/tutorial002_py39/app.py | 30 + .../tutorial002_py39/database.py | 10 + .../tutorial002_py39/hero_model.py | 16 + .../tutorial002_py39/team_model.py | 14 + .../create_tables/tutorial001_py310.py | 34 + .../connect/delete/tutorial001_py310.py | 79 +++ .../connect/insert/tutorial001_py310.py | 67 ++ .../connect/select/tutorial001_py310.py | 76 +++ .../connect/select/tutorial002_py310.py | 76 +++ .../connect/select/tutorial003_py310.py | 76 +++ .../connect/select/tutorial004_py310.py | 76 +++ .../connect/select/tutorial005_py310.py | 76 +++ .../connect/update/tutorial001_py310.py | 73 ++ .../create_db_and_table/tutorial001_py310.py | 16 + .../create_db_and_table/tutorial002_py310.py | 22 + .../create_db_and_table/tutorial003_py310.py | 22 + docs_src/tutorial/delete/tutorial001_py310.py | 98 +++ docs_src/tutorial/delete/tutorial002_py310.py | 99 +++ .../tutorial001/test_extra_coverage.py | 38 ++ .../app_testing/tutorial001_py310/__init__.py | 0 .../annotations/en/test_main_001.md | 17 + .../annotations/en/test_main_002.md | 25 + .../annotations/en/test_main_003.md | 37 + .../annotations/en/test_main_004.md | 26 + .../annotations/en/test_main_005.md | 41 ++ .../annotations/en/test_main_006.md | 23 + .../app_testing/tutorial001_py310/main.py | 104 +++ .../tutorial001_py310/test_extra_coverage.py | 38 ++ .../tutorial001_py310/test_main.py | 125 ++++ .../tutorial001_py310/test_main_001.py | 32 + .../tutorial001_py310/test_main_002.py | 32 + .../tutorial001_py310/test_main_003.py | 33 + .../tutorial001_py310/test_main_004.py | 35 + .../tutorial001_py310/test_main_005.py | 37 + .../tutorial001_py310/test_main_006.py | 41 ++ .../app_testing/tutorial001_py39/__init__.py | 0 .../annotations/en/test_main_001.md | 17 + .../annotations/en/test_main_002.md | 25 + .../annotations/en/test_main_003.md | 37 + .../annotations/en/test_main_004.md | 26 + .../annotations/en/test_main_005.md | 41 ++ .../annotations/en/test_main_006.md | 23 + .../app_testing/tutorial001_py39/main.py | 106 +++ .../tutorial001_py39/test_extra_coverage.py | 38 ++ .../app_testing/tutorial001_py39/test_main.py | 125 ++++ .../tutorial001_py39/test_main_001.py | 32 + .../tutorial001_py39/test_main_002.py | 32 + .../tutorial001_py39/test_main_003.py | 33 + .../tutorial001_py39/test_main_004.py | 35 + .../tutorial001_py39/test_main_005.py | 37 + .../tutorial001_py39/test_main_006.py | 41 ++ .../fastapi/delete/tutorial001_py310.py | 97 +++ .../fastapi/delete/tutorial001_py39.py | 99 +++ .../limit_and_offset/tutorial001_py310.py | 65 ++ .../limit_and_offset/tutorial001_py39.py | 67 ++ .../multiple_models/tutorial001_py310.py | 58 ++ .../multiple_models/tutorial001_py39.py | 60 ++ .../multiple_models/tutorial002_py310.py | 56 ++ .../multiple_models/tutorial002_py39.py | 58 ++ .../fastapi/read_one/tutorial001_py310.py | 65 ++ .../fastapi/read_one/tutorial001_py39.py | 67 ++ .../relationships/tutorial001_py310.py | 199 ++++++ .../fastapi/relationships/tutorial001_py39.py | 201 ++++++ .../response_model/tutorial001_py310.py | 44 ++ .../response_model/tutorial001_py39.py | 46 ++ .../tutorial001_py310.py | 104 +++ .../tutorial001_py39.py | 106 +++ .../simple_hero_api/tutorial001_py310.py | 44 ++ .../fastapi/teams/tutorial001_py310.py | 190 ++++++ .../fastapi/teams/tutorial001_py39.py | 192 ++++++ .../fastapi/update/tutorial001_py310.py | 86 +++ .../fastapi/update/tutorial001_py39.py | 88 +++ .../tutorial/indexes/tutorial001_py310.py | 49 ++ .../tutorial/indexes/tutorial002_py310.py | 57 ++ docs_src/tutorial/insert/tutorial001_py310.py | 43 ++ docs_src/tutorial/insert/tutorial002_py310.py | 40 ++ docs_src/tutorial/insert/tutorial003_py310.py | 41 ++ .../many_to_many/tutorial001_py310.py | 78 +++ .../tutorial/many_to_many/tutorial001_py39.py | 84 +++ .../many_to_many/tutorial002_py310.py | 101 +++ .../tutorial/many_to_many/tutorial002_py39.py | 107 +++ .../many_to_many/tutorial003_py310.py | 117 ++++ .../tutorial/many_to_many/tutorial003_py39.py | 123 ++++ .../offset_and_limit/tutorial001_py310.py | 57 ++ .../offset_and_limit/tutorial002_py310.py | 57 ++ .../offset_and_limit/tutorial003_py310.py | 57 ++ .../offset_and_limit/tutorial004_py310.py | 57 ++ docs_src/tutorial/one/tutorial001_py310.py | 57 ++ docs_src/tutorial/one/tutorial002_py310.py | 57 ++ docs_src/tutorial/one/tutorial003_py310.py | 57 ++ docs_src/tutorial/one/tutorial004_py310.py | 57 ++ docs_src/tutorial/one/tutorial005_py310.py | 57 ++ docs_src/tutorial/one/tutorial006_py310.py | 55 ++ docs_src/tutorial/one/tutorial007_py310.py | 57 ++ docs_src/tutorial/one/tutorial008_py310.py | 55 ++ docs_src/tutorial/one/tutorial009_py310.py | 55 ++ .../back_populates/tutorial001_py310.py | 141 ++++ .../back_populates/tutorial001_py39.py | 143 ++++ .../back_populates/tutorial002_py310.py | 141 ++++ .../back_populates/tutorial002_py39.py | 143 ++++ .../back_populates/tutorial003_py310.py | 57 ++ .../back_populates/tutorial003_py39.py | 59 ++ .../tutorial001_py310.py | 100 +++ .../tutorial001_py39.py | 102 +++ .../tutorial001_py310.py | 68 ++ .../tutorial001_py39.py | 70 ++ .../read_relationships/tutorial001_py310.py | 115 ++++ .../read_relationships/tutorial001_py39.py | 117 ++++ .../read_relationships/tutorial002_py310.py | 125 ++++ .../read_relationships/tutorial002_py39.py | 127 ++++ docs_src/tutorial/select/tutorial001_py310.py | 49 ++ docs_src/tutorial/select/tutorial002_py310.py | 50 ++ docs_src/tutorial/select/tutorial003_py310.py | 49 ++ docs_src/tutorial/select/tutorial004_py310.py | 47 ++ docs_src/tutorial/update/tutorial001_py310.py | 63 ++ docs_src/tutorial/update/tutorial002_py310.py | 63 ++ docs_src/tutorial/update/tutorial003_py310.py | 77 +++ docs_src/tutorial/update/tutorial004_py310.py | 78 +++ docs_src/tutorial/where/tutorial001_py310.py | 49 ++ docs_src/tutorial/where/tutorial002_py310.py | 49 ++ docs_src/tutorial/where/tutorial003_py310.py | 57 ++ docs_src/tutorial/where/tutorial004_py310.py | 57 ++ docs_src/tutorial/where/tutorial005_py310.py | 57 ++ docs_src/tutorial/where/tutorial006_py310.py | 57 ++ docs_src/tutorial/where/tutorial007_py310.py | 57 ++ docs_src/tutorial/where/tutorial008_py310.py | 57 ++ docs_src/tutorial/where/tutorial009_py310.py | 57 ++ docs_src/tutorial/where/tutorial010_py310.py | 57 ++ docs_src/tutorial/where/tutorial011_py310.py | 57 ++ tests/conftest.py | 7 + .../test_decimal/test_tutorial001_py310.py | 45 ++ ...est_tutorial001_py310_tutorial002_py310.py | 163 +++++ .../test_tutorial001_py310.py | 38 ++ .../test_tutorial001_py39.py | 38 ++ .../test_tutorial002_py310.py | 38 ++ .../test_tutorial002_py39.py | 38 ++ .../test_tutorial001_py310.py | 17 + .../test_delete/test_tutorial001_py310.py | 73 ++ .../test_insert/test_tutorial001_py310.py | 53 ++ ...est_tutorial001_py310_tutorial002_py310.py | 92 +++ .../test_select/test_tutorial003_py310.py | 89 +++ .../test_select/test_tutorial004_py310.py | 63 ++ .../test_select/test_tutorial005_py310.py | 65 ++ .../test_update/test_tutorial001_py310.py | 63 ++ .../test_tutorial001_py310.py | 19 + .../test_tutorial002_py310.py | 16 + .../test_tutorial003_py310.py | 16 + ...est_tutorial001_py310_tutorial002_py310.py | 88 +++ .../test_tutorial001_py310_tests_main.py | 25 + .../test_tutorial001_py39_tests_main.py | 25 + .../test_tutorial001_tests_main.py | 105 +-- .../test_delete/test_tutorial001_py310.py | 331 +++++++++ .../test_delete/test_tutorial001_py39.py | 331 +++++++++ .../test_tutorial001_py310.py | 255 +++++++ .../test_tutorial001_py39.py | 255 +++++++ .../test_tutorial001_py310.py | 203 ++++++ .../test_tutorial001_py39.py | 203 ++++++ .../test_tutorial002_py310.py | 203 ++++++ .../test_tutorial002_py39.py | 203 ++++++ .../test_read_one/test_tutorial001_py310.py | 201 ++++++ .../test_read_one/test_tutorial001_py39.py | 201 ++++++ .../test_tutorial001_py310.py | 638 ++++++++++++++++++ .../test_tutorial001_py39.py | 638 ++++++++++++++++++ .../test_tutorial001_py310.py | 144 ++++ .../test_tutorial001_py39.py | 144 ++++ .../test_tutorial001_py310.py | 333 +++++++++ .../test_tutorial001_py39.py | 333 +++++++++ .../test_tutorial001_py310.py | 150 ++++ .../test_teams/test_tutorial001_py310.py | 595 ++++++++++++++++ .../test_teams/test_tutorial001_py39.py | 595 ++++++++++++++++ .../test_update/test_tutorial001_py310.py | 310 +++++++++ .../test_update/test_tutorial001_py39.py | 310 +++++++++ .../test_indexes/test_tutorial001_py310.py | 46 ++ ...est_tutorial006.py => test_tutorial002.py} | 0 .../test_indexes/test_tutorial002_py310.py | 47 ++ .../test_insert/test_tutorial001_py310.py | 30 + .../test_insert/test_tutorial002_py310.py | 30 + .../test_insert/test_tutorial003_py310.py | 30 + .../test_tutorial001_py310.py | 35 + .../test_tutorial002_py310.py | 35 + .../test_tutorial003_py310.py | 33 + .../test_tutorial004_py310.py | 27 + .../test_tutorial001_py310.py | 50 ++ .../test_tutorial001_py39.py | 50 ++ .../test_tutorial002_py310.py | 77 +++ .../test_tutorial002_py39.py | 77 +++ .../test_tutorial003_py310.py | 73 ++ .../test_tutorial003_py39.py | 73 ++ .../test_one/test_tutorial001_py310.py | 30 + .../test_one/test_tutorial002_py310.py | 20 + .../test_one/test_tutorial003_py310.py | 25 + .../test_one/test_tutorial004_py310.py | 41 ++ .../test_one/test_tutorial005_py310.py | 41 ++ .../test_one/test_tutorial006_py310.py | 25 + .../test_one/test_tutorial007_py310.py | 25 + .../test_one/test_tutorial008_py310.py | 25 + .../test_one/test_tutorial009_py310.py | 20 + .../test_tutorial001_py310.py | 290 ++++++++ .../test_tutorial001_py39.py | 290 ++++++++ .../test_tutorial002_py310.py | 280 ++++++++ .../test_tutorial002_py39.py | 280 ++++++++ .../test_tutorial003_py310.py | 21 + .../test_tutorial003_py39.py | 21 + .../test_tutorial001_py310.py | 99 +++ .../test_tutorial001_py39.py | 99 +++ .../test_tutorial001_py310.py | 55 ++ .../test_tutorial001_py39.py | 55 ++ .../test_tutorial001_py310.py | 107 +++ .../test_tutorial001_py39.py | 107 +++ .../test_tutorial002_py310.py | 149 ++++ .../test_tutorial002_py39.py | 149 ++++ ...est_tutorial001_py310_tutorial002_py310.py | 57 ++ ...est_tutorial003_py310_tutorial004_py310.py | 59 ++ ...est_tutorial001_py310_tutorial002_py310.py | 56 ++ ...est_tutorial003_py310_tutorial004_py310.py | 69 ++ .../test_where/test_tutorial001_py310.py | 29 + .../test_where/test_tutorial002_py310.py | 30 + .../test_where/test_tutorial003_py310.py | 37 + .../test_where/test_tutorial004_py310.py | 37 + .../test_where/test_tutorial005_py310.py | 22 + .../test_where/test_tutorial006_py310.py | 23 + .../test_where/test_tutorial007_py310.py | 23 + .../test_where/test_tutorial008_py310.py | 23 + .../test_where/test_tutorial009_py310.py | 31 + .../test_where/test_tutorial010_py310.py | 31 + .../test_where/test_tutorial011_py310.py | 37 + 243 files changed, 20060 insertions(+), 83 deletions(-) create mode 100644 docs_src/advanced/decimal/tutorial001_py310.py create mode 100644 docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py create mode 100644 docs_src/tutorial/automatic_id_none_refresh/tutorial002_py310.py create mode 100644 docs_src/tutorial/code_structure/tutorial001_py310/__init__.py create mode 100644 docs_src/tutorial/code_structure/tutorial001_py310/app.py create mode 100644 docs_src/tutorial/code_structure/tutorial001_py310/database.py create mode 100644 docs_src/tutorial/code_structure/tutorial001_py310/models.py create mode 100644 docs_src/tutorial/code_structure/tutorial001_py39/__init__.py create mode 100644 docs_src/tutorial/code_structure/tutorial001_py39/app.py create mode 100644 docs_src/tutorial/code_structure/tutorial001_py39/database.py create mode 100644 docs_src/tutorial/code_structure/tutorial001_py39/models.py create mode 100644 docs_src/tutorial/code_structure/tutorial002_py310/__init__.py create mode 100644 docs_src/tutorial/code_structure/tutorial002_py310/app.py create mode 100644 docs_src/tutorial/code_structure/tutorial002_py310/database.py create mode 100644 docs_src/tutorial/code_structure/tutorial002_py310/hero_model.py create mode 100644 docs_src/tutorial/code_structure/tutorial002_py310/team_model.py create mode 100644 docs_src/tutorial/code_structure/tutorial002_py39/__init__.py create mode 100644 docs_src/tutorial/code_structure/tutorial002_py39/app.py create mode 100644 docs_src/tutorial/code_structure/tutorial002_py39/database.py create mode 100644 docs_src/tutorial/code_structure/tutorial002_py39/hero_model.py create mode 100644 docs_src/tutorial/code_structure/tutorial002_py39/team_model.py create mode 100644 docs_src/tutorial/connect/create_tables/tutorial001_py310.py create mode 100644 docs_src/tutorial/connect/delete/tutorial001_py310.py create mode 100644 docs_src/tutorial/connect/insert/tutorial001_py310.py create mode 100644 docs_src/tutorial/connect/select/tutorial001_py310.py create mode 100644 docs_src/tutorial/connect/select/tutorial002_py310.py create mode 100644 docs_src/tutorial/connect/select/tutorial003_py310.py create mode 100644 docs_src/tutorial/connect/select/tutorial004_py310.py create mode 100644 docs_src/tutorial/connect/select/tutorial005_py310.py create mode 100644 docs_src/tutorial/connect/update/tutorial001_py310.py create mode 100644 docs_src/tutorial/create_db_and_table/tutorial001_py310.py create mode 100644 docs_src/tutorial/create_db_and_table/tutorial002_py310.py create mode 100644 docs_src/tutorial/create_db_and_table/tutorial003_py310.py create mode 100644 docs_src/tutorial/delete/tutorial001_py310.py create mode 100644 docs_src/tutorial/delete/tutorial002_py310.py create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001/test_extra_coverage.py create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001_py310/__init__.py create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_001.md create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_002.md create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_003.md create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_004.md create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_005.md create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_006.md create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001_py310/main.py create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_extra_coverage.py create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main.py create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_001.py create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_002.py create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_003.py create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_004.py create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_005.py create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_006.py create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001_py39/__init__.py create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_001.md create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_002.md create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_003.md create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_004.md create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_005.md create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_006.md create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001_py39/main.py create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_extra_coverage.py create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main.py create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_001.py create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_002.py create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_003.py create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_004.py create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_005.py create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_006.py create mode 100644 docs_src/tutorial/fastapi/delete/tutorial001_py310.py create mode 100644 docs_src/tutorial/fastapi/delete/tutorial001_py39.py create mode 100644 docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py310.py create mode 100644 docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py39.py create mode 100644 docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py create mode 100644 docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py create mode 100644 docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py create mode 100644 docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py create mode 100644 docs_src/tutorial/fastapi/read_one/tutorial001_py310.py create mode 100644 docs_src/tutorial/fastapi/read_one/tutorial001_py39.py create mode 100644 docs_src/tutorial/fastapi/relationships/tutorial001_py310.py create mode 100644 docs_src/tutorial/fastapi/relationships/tutorial001_py39.py create mode 100644 docs_src/tutorial/fastapi/response_model/tutorial001_py310.py create mode 100644 docs_src/tutorial/fastapi/response_model/tutorial001_py39.py create mode 100644 docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py create mode 100644 docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py create mode 100644 docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py create mode 100644 docs_src/tutorial/fastapi/teams/tutorial001_py310.py create mode 100644 docs_src/tutorial/fastapi/teams/tutorial001_py39.py create mode 100644 docs_src/tutorial/fastapi/update/tutorial001_py310.py create mode 100644 docs_src/tutorial/fastapi/update/tutorial001_py39.py create mode 100644 docs_src/tutorial/indexes/tutorial001_py310.py create mode 100644 docs_src/tutorial/indexes/tutorial002_py310.py create mode 100644 docs_src/tutorial/insert/tutorial001_py310.py create mode 100644 docs_src/tutorial/insert/tutorial002_py310.py create mode 100644 docs_src/tutorial/insert/tutorial003_py310.py create mode 100644 docs_src/tutorial/many_to_many/tutorial001_py310.py create mode 100644 docs_src/tutorial/many_to_many/tutorial001_py39.py create mode 100644 docs_src/tutorial/many_to_many/tutorial002_py310.py create mode 100644 docs_src/tutorial/many_to_many/tutorial002_py39.py create mode 100644 docs_src/tutorial/many_to_many/tutorial003_py310.py create mode 100644 docs_src/tutorial/many_to_many/tutorial003_py39.py create mode 100644 docs_src/tutorial/offset_and_limit/tutorial001_py310.py create mode 100644 docs_src/tutorial/offset_and_limit/tutorial002_py310.py create mode 100644 docs_src/tutorial/offset_and_limit/tutorial003_py310.py create mode 100644 docs_src/tutorial/offset_and_limit/tutorial004_py310.py create mode 100644 docs_src/tutorial/one/tutorial001_py310.py create mode 100644 docs_src/tutorial/one/tutorial002_py310.py create mode 100644 docs_src/tutorial/one/tutorial003_py310.py create mode 100644 docs_src/tutorial/one/tutorial004_py310.py create mode 100644 docs_src/tutorial/one/tutorial005_py310.py create mode 100644 docs_src/tutorial/one/tutorial006_py310.py create mode 100644 docs_src/tutorial/one/tutorial007_py310.py create mode 100644 docs_src/tutorial/one/tutorial008_py310.py create mode 100644 docs_src/tutorial/one/tutorial009_py310.py create mode 100644 docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py create mode 100644 docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py create mode 100644 docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py create mode 100644 docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py39.py create mode 100644 docs_src/tutorial/relationship_attributes/back_populates/tutorial003_py310.py create mode 100644 docs_src/tutorial/relationship_attributes/back_populates/tutorial003_py39.py create mode 100644 docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py create mode 100644 docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py39.py create mode 100644 docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py create mode 100644 docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py39.py create mode 100644 docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py310.py create mode 100644 docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py39.py create mode 100644 docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py310.py create mode 100644 docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py39.py create mode 100644 docs_src/tutorial/select/tutorial001_py310.py create mode 100644 docs_src/tutorial/select/tutorial002_py310.py create mode 100644 docs_src/tutorial/select/tutorial003_py310.py create mode 100644 docs_src/tutorial/select/tutorial004_py310.py create mode 100644 docs_src/tutorial/update/tutorial001_py310.py create mode 100644 docs_src/tutorial/update/tutorial002_py310.py create mode 100644 docs_src/tutorial/update/tutorial003_py310.py create mode 100644 docs_src/tutorial/update/tutorial004_py310.py create mode 100644 docs_src/tutorial/where/tutorial001_py310.py create mode 100644 docs_src/tutorial/where/tutorial002_py310.py create mode 100644 docs_src/tutorial/where/tutorial003_py310.py create mode 100644 docs_src/tutorial/where/tutorial004_py310.py create mode 100644 docs_src/tutorial/where/tutorial005_py310.py create mode 100644 docs_src/tutorial/where/tutorial006_py310.py create mode 100644 docs_src/tutorial/where/tutorial007_py310.py create mode 100644 docs_src/tutorial/where/tutorial008_py310.py create mode 100644 docs_src/tutorial/where/tutorial009_py310.py create mode 100644 docs_src/tutorial/where/tutorial010_py310.py create mode 100644 docs_src/tutorial/where/tutorial011_py310.py create mode 100644 tests/test_advanced/test_decimal/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_automatic_id_none_refresh/test_tutorial001_py310_tutorial002_py310.py create mode 100644 tests/test_tutorial/test_code_structure/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_code_structure/test_tutorial001_py39.py create mode 100644 tests/test_tutorial/test_code_structure/test_tutorial002_py310.py create mode 100644 tests/test_tutorial/test_code_structure/test_tutorial002_py39.py create mode 100644 tests/test_tutorial/test_connect/test_create_connected_tables/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_connect/test_delete/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_connect/test_insert/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_connect/test_select/test_tutorial001_py310_tutorial002_py310.py create mode 100644 tests/test_tutorial/test_connect/test_select/test_tutorial003_py310.py create mode 100644 tests/test_tutorial/test_connect/test_select/test_tutorial004_py310.py create mode 100644 tests/test_tutorial/test_connect/test_select/test_tutorial005_py310.py create mode 100644 tests/test_tutorial/test_connect/test_update/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_create_db_and_table/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_create_db_and_table/test_tutorial002_py310.py create mode 100644 tests/test_tutorial/test_create_db_and_table/test_tutorial003_py310.py create mode 100644 tests/test_tutorial/test_delete/test_tutorial001_py310_tutorial002_py310.py create mode 100644 tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_py310_tests_main.py create mode 100644 tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_py39_tests_main.py create mode 100644 tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py39.py create mode 100644 tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py39.py create mode 100644 tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py39.py create mode 100644 tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py310.py create mode 100644 tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py39.py create mode 100644 tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py39.py create mode 100644 tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py39.py create mode 100644 tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py39.py create mode 100644 tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py39.py create mode 100644 tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py39.py create mode 100644 tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py39.py create mode 100644 tests/test_tutorial/test_indexes/test_tutorial001_py310.py rename tests/test_tutorial/test_indexes/{test_tutorial006.py => test_tutorial002.py} (100%) create mode 100644 tests/test_tutorial/test_indexes/test_tutorial002_py310.py create mode 100644 tests/test_tutorial/test_insert/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_insert/test_tutorial002_py310.py create mode 100644 tests/test_tutorial/test_insert/test_tutorial003_py310.py create mode 100644 tests/test_tutorial/test_limit_and_offset/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_limit_and_offset/test_tutorial002_py310.py create mode 100644 tests/test_tutorial/test_limit_and_offset/test_tutorial003_py310.py create mode 100644 tests/test_tutorial/test_limit_and_offset/test_tutorial004_py310.py create mode 100644 tests/test_tutorial/test_many_to_many/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_many_to_many/test_tutorial001_py39.py create mode 100644 tests/test_tutorial/test_many_to_many/test_tutorial002_py310.py create mode 100644 tests/test_tutorial/test_many_to_many/test_tutorial002_py39.py create mode 100644 tests/test_tutorial/test_many_to_many/test_tutorial003_py310.py create mode 100644 tests/test_tutorial/test_many_to_many/test_tutorial003_py39.py create mode 100644 tests/test_tutorial/test_one/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_one/test_tutorial002_py310.py create mode 100644 tests/test_tutorial/test_one/test_tutorial003_py310.py create mode 100644 tests/test_tutorial/test_one/test_tutorial004_py310.py create mode 100644 tests/test_tutorial/test_one/test_tutorial005_py310.py create mode 100644 tests/test_tutorial/test_one/test_tutorial006_py310.py create mode 100644 tests/test_tutorial/test_one/test_tutorial007_py310.py create mode 100644 tests/test_tutorial/test_one/test_tutorial008_py310.py create mode 100644 tests/test_tutorial/test_one/test_tutorial009_py310.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001_py39.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002_py310.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002_py39.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003_py310.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003_py39.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001_py39.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001_py39.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001_py39.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002_py310.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002_py39.py create mode 100644 tests/test_tutorial/test_select/test_tutorial001_py310_tutorial002_py310.py create mode 100644 tests/test_tutorial/test_select/test_tutorial003_py310_tutorial004_py310.py create mode 100644 tests/test_tutorial/test_update/test_tutorial001_py310_tutorial002_py310.py create mode 100644 tests/test_tutorial/test_update/test_tutorial003_py310_tutorial004_py310.py create mode 100644 tests/test_tutorial/test_where/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_where/test_tutorial002_py310.py create mode 100644 tests/test_tutorial/test_where/test_tutorial003_py310.py create mode 100644 tests/test_tutorial/test_where/test_tutorial004_py310.py create mode 100644 tests/test_tutorial/test_where/test_tutorial005_py310.py create mode 100644 tests/test_tutorial/test_where/test_tutorial006_py310.py create mode 100644 tests/test_tutorial/test_where/test_tutorial007_py310.py create mode 100644 tests/test_tutorial/test_where/test_tutorial008_py310.py create mode 100644 tests/test_tutorial/test_where/test_tutorial009_py310.py create mode 100644 tests/test_tutorial/test_where/test_tutorial010_py310.py create mode 100644 tests/test_tutorial/test_where/test_tutorial011_py310.py diff --git a/docs_src/advanced/decimal/tutorial001_py310.py b/docs_src/advanced/decimal/tutorial001_py310.py new file mode 100644 index 0000000000..92afc09012 --- /dev/null +++ b/docs_src/advanced/decimal/tutorial001_py310.py @@ -0,0 +1,59 @@ +from pydantic import condecimal +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + money: condecimal(max_digits=5, decimal_places=3) = Field(default=0) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson", money=1.1) + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador", money=0.001) + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48, money=2.2) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Deadpond") + results = session.exec(statement) + hero_1 = results.one() + print("Hero 1:", hero_1) + + statement = select(Hero).where(Hero.name == "Rusty-Man") + results = session.exec(statement) + hero_2 = results.one() + print("Hero 2:", hero_2) + + total_money = hero_1.money + hero_2.money + print(f"Total money: {total_money}") + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py b/docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py new file mode 100644 index 0000000000..6b76da58ff --- /dev/null +++ b/docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py @@ -0,0 +1,79 @@ +from sqlmodel import Field, Session, SQLModel, create_engine + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + + print("Before interacting with the database") + print("Hero 1:", hero_1) + print("Hero 2:", hero_2) + print("Hero 3:", hero_3) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + + print("After adding to the session") + print("Hero 1:", hero_1) + print("Hero 2:", hero_2) + print("Hero 3:", hero_3) + + session.commit() + + print("After committing the session") + print("Hero 1:", hero_1) + print("Hero 2:", hero_2) + print("Hero 3:", hero_3) + + print("After committing the session, show IDs") + print("Hero 1 ID:", hero_1.id) + print("Hero 2 ID:", hero_2.id) + print("Hero 3 ID:", hero_3.id) + + print("After committing the session, show names") + print("Hero 1 name:", hero_1.name) + print("Hero 2 name:", hero_2.name) + print("Hero 3 name:", hero_3.name) + + session.refresh(hero_1) + session.refresh(hero_2) + session.refresh(hero_3) + + print("After refreshing the heroes") + print("Hero 1:", hero_1) + print("Hero 2:", hero_2) + print("Hero 3:", hero_3) + + print("After the session closes") + print("Hero 1:", hero_1) + print("Hero 2:", hero_2) + print("Hero 3:", hero_3) + + +def main(): + create_db_and_tables() + create_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/automatic_id_none_refresh/tutorial002_py310.py b/docs_src/tutorial/automatic_id_none_refresh/tutorial002_py310.py new file mode 100644 index 0000000000..0f3ad44cf1 --- /dev/null +++ b/docs_src/tutorial/automatic_id_none_refresh/tutorial002_py310.py @@ -0,0 +1,80 @@ +from sqlmodel import Field, Session, SQLModel, create_engine + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") # (1)! + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") # (2)! + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) # (3)! + + print("Before interacting with the database") # (4)! + print("Hero 1:", hero_1) # (5)! + print("Hero 2:", hero_2) # (6)! + print("Hero 3:", hero_3) # (7)! + + with Session(engine) as session: # (8)! + session.add(hero_1) # (9)! + session.add(hero_2) # (10)! + session.add(hero_3) # (11)! + + print("After adding to the session") # (12)! + print("Hero 1:", hero_1) # (13)! + print("Hero 2:", hero_2) # (14)! + print("Hero 3:", hero_3) # (15)! + + session.commit() # (16)! + + print("After committing the session") # (17)! + print("Hero 1:", hero_1) # (18)! + print("Hero 2:", hero_2) # (19)! + print("Hero 3:", hero_3) # (20)! + + print("After committing the session, show IDs") # (21)! + print("Hero 1 ID:", hero_1.id) # (22)! + print("Hero 2 ID:", hero_2.id) # (23)! + print("Hero 3 ID:", hero_3.id) # (24)! + + print("After committing the session, show names") # (25)! + print("Hero 1 name:", hero_1.name) # (26)! + print("Hero 2 name:", hero_2.name) # (27)! + print("Hero 3 name:", hero_3.name) # (28)! + + session.refresh(hero_1) # (29)! + session.refresh(hero_2) # (30)! + session.refresh(hero_3) # (31)! + + print("After refreshing the heroes") # (32)! + print("Hero 1:", hero_1) # (33)! + print("Hero 2:", hero_2) # (34)! + print("Hero 3:", hero_3) # (35)! + # (36)! + + print("After the session closes") # (37)! + print("Hero 1:", hero_1) # (38)! + print("Hero 2:", hero_2) # (39)! + print("Hero 3:", hero_3) # (40)! + + +def main(): + create_db_and_tables() + create_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/code_structure/tutorial001_py310/__init__.py b/docs_src/tutorial/code_structure/tutorial001_py310/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/tutorial/code_structure/tutorial001_py310/app.py b/docs_src/tutorial/code_structure/tutorial001_py310/app.py new file mode 100644 index 0000000000..065f8a78b5 --- /dev/null +++ b/docs_src/tutorial/code_structure/tutorial001_py310/app.py @@ -0,0 +1,29 @@ +from sqlmodel import Session + +from .database import create_db_and_tables, engine +from .models import Hero, Team + + +def create_heroes(): + with Session(engine) as session: + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + session.add(hero_deadpond) + session.commit() + + session.refresh(hero_deadpond) + + print("Created hero:", hero_deadpond) + print("Hero's team:", hero_deadpond.team) + + +def main(): + create_db_and_tables() + create_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/code_structure/tutorial001_py310/database.py b/docs_src/tutorial/code_structure/tutorial001_py310/database.py new file mode 100644 index 0000000000..d6de16c11f --- /dev/null +++ b/docs_src/tutorial/code_structure/tutorial001_py310/database.py @@ -0,0 +1,10 @@ +from sqlmodel import SQLModel, create_engine + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) diff --git a/docs_src/tutorial/code_structure/tutorial001_py310/models.py b/docs_src/tutorial/code_structure/tutorial001_py310/models.py new file mode 100644 index 0000000000..1f485ef0b1 --- /dev/null +++ b/docs_src/tutorial/code_structure/tutorial001_py310/models.py @@ -0,0 +1,19 @@ +from sqlmodel import Field, Relationship, SQLModel + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team") + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + team: Team | None = Relationship(back_populates="heroes") diff --git a/docs_src/tutorial/code_structure/tutorial001_py39/__init__.py b/docs_src/tutorial/code_structure/tutorial001_py39/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/tutorial/code_structure/tutorial001_py39/app.py b/docs_src/tutorial/code_structure/tutorial001_py39/app.py new file mode 100644 index 0000000000..065f8a78b5 --- /dev/null +++ b/docs_src/tutorial/code_structure/tutorial001_py39/app.py @@ -0,0 +1,29 @@ +from sqlmodel import Session + +from .database import create_db_and_tables, engine +from .models import Hero, Team + + +def create_heroes(): + with Session(engine) as session: + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + session.add(hero_deadpond) + session.commit() + + session.refresh(hero_deadpond) + + print("Created hero:", hero_deadpond) + print("Hero's team:", hero_deadpond.team) + + +def main(): + create_db_and_tables() + create_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/code_structure/tutorial001_py39/database.py b/docs_src/tutorial/code_structure/tutorial001_py39/database.py new file mode 100644 index 0000000000..d6de16c11f --- /dev/null +++ b/docs_src/tutorial/code_structure/tutorial001_py39/database.py @@ -0,0 +1,10 @@ +from sqlmodel import SQLModel, create_engine + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) diff --git a/docs_src/tutorial/code_structure/tutorial001_py39/models.py b/docs_src/tutorial/code_structure/tutorial001_py39/models.py new file mode 100644 index 0000000000..ff6b6c2d66 --- /dev/null +++ b/docs_src/tutorial/code_structure/tutorial001_py39/models.py @@ -0,0 +1,21 @@ +from typing import Optional + +from sqlmodel import Field, Relationship, SQLModel + + +class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team") + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + team_id: Optional[int] = Field(default=None, foreign_key="team.id") + team: Optional[Team] = Relationship(back_populates="heroes") diff --git a/docs_src/tutorial/code_structure/tutorial002_py310/__init__.py b/docs_src/tutorial/code_structure/tutorial002_py310/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/tutorial/code_structure/tutorial002_py310/app.py b/docs_src/tutorial/code_structure/tutorial002_py310/app.py new file mode 100644 index 0000000000..8afaee7c16 --- /dev/null +++ b/docs_src/tutorial/code_structure/tutorial002_py310/app.py @@ -0,0 +1,30 @@ +from sqlmodel import Session + +from .database import create_db_and_tables, engine +from .hero_model import Hero +from .team_model import Team + + +def create_heroes(): + with Session(engine) as session: + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + session.add(hero_deadpond) + session.commit() + + session.refresh(hero_deadpond) + + print("Created hero:", hero_deadpond) + print("Hero's team:", hero_deadpond.team) + + +def main(): + create_db_and_tables() + create_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/code_structure/tutorial002_py310/database.py b/docs_src/tutorial/code_structure/tutorial002_py310/database.py new file mode 100644 index 0000000000..d6de16c11f --- /dev/null +++ b/docs_src/tutorial/code_structure/tutorial002_py310/database.py @@ -0,0 +1,10 @@ +from sqlmodel import SQLModel, create_engine + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) diff --git a/docs_src/tutorial/code_structure/tutorial002_py310/hero_model.py b/docs_src/tutorial/code_structure/tutorial002_py310/hero_model.py new file mode 100644 index 0000000000..52fe68be16 --- /dev/null +++ b/docs_src/tutorial/code_structure/tutorial002_py310/hero_model.py @@ -0,0 +1,16 @@ +from typing import TYPE_CHECKING, Optional + +from sqlmodel import Field, Relationship, SQLModel + +if TYPE_CHECKING: + from .team_model import Team + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + team: Optional["Team"] = Relationship(back_populates="heroes") diff --git a/docs_src/tutorial/code_structure/tutorial002_py310/team_model.py b/docs_src/tutorial/code_structure/tutorial002_py310/team_model.py new file mode 100644 index 0000000000..10af5b9c2f --- /dev/null +++ b/docs_src/tutorial/code_structure/tutorial002_py310/team_model.py @@ -0,0 +1,14 @@ +from typing import TYPE_CHECKING + +from sqlmodel import Field, Relationship, SQLModel + +if TYPE_CHECKING: + from .hero_model import Hero + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team") diff --git a/docs_src/tutorial/code_structure/tutorial002_py39/__init__.py b/docs_src/tutorial/code_structure/tutorial002_py39/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/tutorial/code_structure/tutorial002_py39/app.py b/docs_src/tutorial/code_structure/tutorial002_py39/app.py new file mode 100644 index 0000000000..8afaee7c16 --- /dev/null +++ b/docs_src/tutorial/code_structure/tutorial002_py39/app.py @@ -0,0 +1,30 @@ +from sqlmodel import Session + +from .database import create_db_and_tables, engine +from .hero_model import Hero +from .team_model import Team + + +def create_heroes(): + with Session(engine) as session: + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + session.add(hero_deadpond) + session.commit() + + session.refresh(hero_deadpond) + + print("Created hero:", hero_deadpond) + print("Hero's team:", hero_deadpond.team) + + +def main(): + create_db_and_tables() + create_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/code_structure/tutorial002_py39/database.py b/docs_src/tutorial/code_structure/tutorial002_py39/database.py new file mode 100644 index 0000000000..d6de16c11f --- /dev/null +++ b/docs_src/tutorial/code_structure/tutorial002_py39/database.py @@ -0,0 +1,10 @@ +from sqlmodel import SQLModel, create_engine + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) diff --git a/docs_src/tutorial/code_structure/tutorial002_py39/hero_model.py b/docs_src/tutorial/code_structure/tutorial002_py39/hero_model.py new file mode 100644 index 0000000000..06dd9c6dfd --- /dev/null +++ b/docs_src/tutorial/code_structure/tutorial002_py39/hero_model.py @@ -0,0 +1,16 @@ +from typing import TYPE_CHECKING, Optional + +from sqlmodel import Field, Relationship, SQLModel + +if TYPE_CHECKING: + from .team_model import Team + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + team_id: Optional[int] = Field(default=None, foreign_key="team.id") + team: Optional["Team"] = Relationship(back_populates="heroes") diff --git a/docs_src/tutorial/code_structure/tutorial002_py39/team_model.py b/docs_src/tutorial/code_structure/tutorial002_py39/team_model.py new file mode 100644 index 0000000000..b51c070cf1 --- /dev/null +++ b/docs_src/tutorial/code_structure/tutorial002_py39/team_model.py @@ -0,0 +1,14 @@ +from typing import TYPE_CHECKING, Optional + +from sqlmodel import Field, Relationship, SQLModel + +if TYPE_CHECKING: + from .hero_model import Hero + + +class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team") diff --git a/docs_src/tutorial/connect/create_tables/tutorial001_py310.py b/docs_src/tutorial/connect/create_tables/tutorial001_py310.py new file mode 100644 index 0000000000..460b9768d7 --- /dev/null +++ b/docs_src/tutorial/connect/create_tables/tutorial001_py310.py @@ -0,0 +1,34 @@ +from sqlmodel import Field, SQLModel, create_engine + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def main(): + create_db_and_tables() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/connect/delete/tutorial001_py310.py b/docs_src/tutorial/connect/delete/tutorial001_py310.py new file mode 100644 index 0000000000..4815ad4e34 --- /dev/null +++ b/docs_src/tutorial/connect/delete/tutorial001_py310.py @@ -0,0 +1,79 @@ +from sqlmodel import Field, Session, SQLModel, create_engine + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) 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) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + 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) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_spider_boy.team_id = None + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("No longer Preventer:", hero_spider_boy) + + +def main(): + create_db_and_tables() + create_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/connect/insert/tutorial001_py310.py b/docs_src/tutorial/connect/insert/tutorial001_py310.py new file mode 100644 index 0000000000..506429dc1a --- /dev/null +++ b/docs_src/tutorial/connect/insert/tutorial001_py310.py @@ -0,0 +1,67 @@ +from sqlmodel import Field, Session, SQLModel, create_engine + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) 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) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + +def main(): + create_db_and_tables() + create_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/connect/select/tutorial001_py310.py b/docs_src/tutorial/connect/select/tutorial001_py310.py new file mode 100644 index 0000000000..c39894fce9 --- /dev/null +++ b/docs_src/tutorial/connect/select/tutorial001_py310.py @@ -0,0 +1,76 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) 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) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero, Team).where(Hero.team_id == Team.id) + results = session.exec(statement) + for hero, team in results: + print("Hero:", hero, "Team:", team) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/connect/select/tutorial002_py310.py b/docs_src/tutorial/connect/select/tutorial002_py310.py new file mode 100644 index 0000000000..756dab098b --- /dev/null +++ b/docs_src/tutorial/connect/select/tutorial002_py310.py @@ -0,0 +1,76 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) 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) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero, Team).join(Team) + results = session.exec(statement) + for hero, team in results: + print("Hero:", hero, "Team:", team) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/connect/select/tutorial003_py310.py b/docs_src/tutorial/connect/select/tutorial003_py310.py new file mode 100644 index 0000000000..63f5b27350 --- /dev/null +++ b/docs_src/tutorial/connect/select/tutorial003_py310.py @@ -0,0 +1,76 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) 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) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero, Team).join(Team, isouter=True) + results = session.exec(statement) + for hero, team in results: + print("Hero:", hero, "Team:", team) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/connect/select/tutorial004_py310.py b/docs_src/tutorial/connect/select/tutorial004_py310.py new file mode 100644 index 0000000000..8b024321d2 --- /dev/null +++ b/docs_src/tutorial/connect/select/tutorial004_py310.py @@ -0,0 +1,76 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) 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) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).join(Team).where(Team.name == "Preventers") + results = session.exec(statement) + for hero in results: + print("Preventer Hero:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/connect/select/tutorial005_py310.py b/docs_src/tutorial/connect/select/tutorial005_py310.py new file mode 100644 index 0000000000..2120640bc4 --- /dev/null +++ b/docs_src/tutorial/connect/select/tutorial005_py310.py @@ -0,0 +1,76 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) 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) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero, Team).join(Team).where(Team.name == "Preventers") + results = session.exec(statement) + for hero, team in results: + print("Preventer Hero:", hero, "Team:", team) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/connect/update/tutorial001_py310.py b/docs_src/tutorial/connect/update/tutorial001_py310.py new file mode 100644 index 0000000000..1fbb108ba2 --- /dev/null +++ b/docs_src/tutorial/connect/update/tutorial001_py310.py @@ -0,0 +1,73 @@ +from sqlmodel import Field, Session, SQLModel, create_engine + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) 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) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + 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) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + +def main(): + create_db_and_tables() + create_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/create_db_and_table/tutorial001_py310.py b/docs_src/tutorial/create_db_and_table/tutorial001_py310.py new file mode 100644 index 0000000000..13ae331bf1 --- /dev/null +++ b/docs_src/tutorial/create_db_and_table/tutorial001_py310.py @@ -0,0 +1,16 @@ +from sqlmodel import Field, SQLModel, create_engine + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + +SQLModel.metadata.create_all(engine) diff --git a/docs_src/tutorial/create_db_and_table/tutorial002_py310.py b/docs_src/tutorial/create_db_and_table/tutorial002_py310.py new file mode 100644 index 0000000000..50c7241ef4 --- /dev/null +++ b/docs_src/tutorial/create_db_and_table/tutorial002_py310.py @@ -0,0 +1,22 @@ +from sqlmodel import Field, SQLModel, create_engine + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +if __name__ == "__main__": + create_db_and_tables() diff --git a/docs_src/tutorial/create_db_and_table/tutorial003_py310.py b/docs_src/tutorial/create_db_and_table/tutorial003_py310.py new file mode 100644 index 0000000000..01fcb14ac2 --- /dev/null +++ b/docs_src/tutorial/create_db_and_table/tutorial003_py310.py @@ -0,0 +1,22 @@ +from sqlmodel import Field, SQLModel, create_engine # (2)! + + +class Hero(SQLModel, table=True): # (3)! + id: int | None = Field(default=None, primary_key=True) # (4)! + name: str # (5)! + secret_name: str # (6)! + age: int | None = None # (7)! + + +sqlite_file_name = "database.db" # (8)! +sqlite_url = f"sqlite:///{sqlite_file_name}" # (9)! + +engine = create_engine(sqlite_url, echo=True) # (10)! + + +def create_db_and_tables(): # (11)! + SQLModel.metadata.create_all(engine) # (12)! + + +if __name__ == "__main__": # (13)! + create_db_and_tables() # (14)! diff --git a/docs_src/tutorial/delete/tutorial001_py310.py b/docs_src/tutorial/delete/tutorial001_py310.py new file mode 100644 index 0000000000..7c29efa3c3 --- /dev/null +++ b/docs_src/tutorial/delete/tutorial001_py310.py @@ -0,0 +1,98 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def update_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Spider-Boy") + results = session.exec(statement) + hero_1 = results.one() + print("Hero 1:", hero_1) + + statement = select(Hero).where(Hero.name == "Captain North America") + results = session.exec(statement) + hero_2 = results.one() + print("Hero 2:", hero_2) + + hero_1.age = 16 + hero_1.name = "Spider-Youngster" + session.add(hero_1) + + hero_2.name = "Captain North America Except Canada" + hero_2.age = 110 + session.add(hero_2) + + session.commit() + session.refresh(hero_1) + session.refresh(hero_2) + + print("Updated hero 1:", hero_1) + print("Updated hero 2:", hero_2) + + +def delete_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Spider-Youngster") + results = session.exec(statement) + hero = results.one() + print("Hero: ", hero) + + session.delete(hero) + session.commit() + + print("Deleted hero:", hero) + + statement = select(Hero).where(Hero.name == "Spider-Youngster") + results = session.exec(statement) + hero = results.first() + + if hero is None: + print("There's no hero named Spider-Youngster") + + +def main(): + create_db_and_tables() + create_heroes() + update_heroes() + delete_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/delete/tutorial002_py310.py b/docs_src/tutorial/delete/tutorial002_py310.py new file mode 100644 index 0000000000..afe9a4764d --- /dev/null +++ b/docs_src/tutorial/delete/tutorial002_py310.py @@ -0,0 +1,99 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def update_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Spider-Boy") + results = session.exec(statement) + hero_1 = results.one() + print("Hero 1:", hero_1) + + statement = select(Hero).where(Hero.name == "Captain North America") + results = session.exec(statement) + hero_2 = results.one() + print("Hero 2:", hero_2) + + hero_1.age = 16 + hero_1.name = "Spider-Youngster" + session.add(hero_1) + + hero_2.name = "Captain North America Except Canada" + hero_2.age = 110 + session.add(hero_2) + + session.commit() + session.refresh(hero_1) + session.refresh(hero_2) + + print("Updated hero 1:", hero_1) + print("Updated hero 2:", hero_2) + + +def delete_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Spider-Youngster") # (1)! + results = session.exec(statement) # (2)! + hero = results.one() # (3)! + print("Hero: ", hero) # (4)! + + session.delete(hero) # (5)! + session.commit() # (6)! + + print("Deleted hero:", hero) # (7)! + + statement = select(Hero).where(Hero.name == "Spider-Youngster") # (8)! + results = session.exec(statement) # (9)! + hero = results.first() # (10)! + + if hero is None: # (11)! + print("There's no hero named Spider-Youngster") # (12)! + # (13)! + + +def main(): + create_db_and_tables() + create_heroes() + update_heroes() + delete_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_extra_coverage.py b/docs_src/tutorial/fastapi/app_testing/tutorial001/test_extra_coverage.py new file mode 100644 index 0000000000..1d8153ab9f --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001/test_extra_coverage.py @@ -0,0 +1,38 @@ +from fastapi.testclient import TestClient +from sqlalchemy import Inspector, inspect +from sqlmodel import Session, create_engine + +from . import main as app_mod +from .test_main import client_fixture, session_fixture + +assert client_fixture, "This keeps the client fixture used below" +assert session_fixture, "This keeps the session fixture used by client_fixture" + + +def test_startup(): + app_mod.engine = create_engine("sqlite://") + app_mod.on_startup() + insp: Inspector = inspect(app_mod.engine) + assert insp.has_table(str(app_mod.Hero.__tablename__)) + + +def test_get_session(): + app_mod.engine = create_engine("sqlite://") + for session in app_mod.get_session(): + assert isinstance(session, Session) + assert session.bind == app_mod.engine + + +def test_read_hero_not_found(client: TestClient): + response = client.get("/heroes/9000") + assert response.status_code == 404 + + +def test_update_hero_not_found(client: TestClient): + response = client.patch("/heroes/9000", json={"name": "Very-Rusty-Man"}) + assert response.status_code == 404 + + +def test_delete_hero_not_found(client: TestClient): + response = client.delete("/heroes/9000") + assert response.status_code == 404 diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/__init__.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_001.md b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_001.md new file mode 100644 index 0000000000..936b84b92d --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_001.md @@ -0,0 +1,17 @@ +1. Import the `app` from the the `main` module. + +2. We create a `TestClient` for the FastAPI `app` and put it in the variable `client`. + +3. Then we use use this `client` to **talk to the API** and send a `POST` HTTP operation, creating a new hero. + +4. Then we get the **JSON data** from the response and put it in the variable `data`. + +5. Next we start testing the results with `assert` statements, we check that the status code of the response is `200`. + +6. We check that the `name` of the hero created is `"Deadpond"`. + +7. We check that the `secret_name` of the hero created is `"Dive Wilson"`. + +8. We check that the `age` of the hero created is `None`, because we didn't send an age. + +9. We check that the hero created has an `id` created by the database, so it's not `None`. diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_002.md b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_002.md new file mode 100644 index 0000000000..0f8555a8dd --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_002.md @@ -0,0 +1,25 @@ +1. Import the `get_session` dependency from the the `main` module. + +2. Define the new function that will be the new **dependency override**. + +3. This function will return a different **session** than the one that would be returned by the original `get_session` function. + + We haven't seen how this new **session** object is created yet, but the point is that this is a different session than the original one from the app. + + This session is attached to a different **engine**, and that different **engine** uses a different URL, for a database just for testing. + + We haven't defined that new **URL** nor the new **engine** yet, but here we already see the that this object `session` will override the one returned by the original dependency `get_session()`. + +4. Then, the FastAPI `app` object has an attribute `app.dependency_overrides`. + + This attribute is a dictionary, and we can put dependency overrides in it by passing, as the **key**, the **original dependency function**, and as the **value**, the **new overriding dependency function**. + + So, here we are telling the FastAPI app to use `get_session_override` instead of `get_session` in all the places in the code that depend on `get_session`, that is, all the parameters with something like: + + ```Python + session: Session = Depends(get_session) + ``` + +5. After we are done with the dependency override, we can restore the application back to normal, by removing all the values in this dictionary `app.dependency_overrides`. + + This way whenever a *path operation function* needs the dependency FastAPI will use the original one instead of the override. diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_003.md b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_003.md new file mode 100644 index 0000000000..2b48ebdacf --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_003.md @@ -0,0 +1,37 @@ +1. Here's a subtle thing to notice. + + Remember that [Order Matters](../create-db-and-table.md#sqlmodel-metadata-order-matters){.internal-link target=_blank} and we need to make sure all the **SQLModel** models are already defined and **imported** before calling `.create_all()`. + + IN this line, by importing something, *anything*, from `.main`, the code in `.main` will be executed, including the definition of the **table models**, and that will automatically register them in `SQLModel.metadata`. + +2. Here we create a new **engine**, completely different from the one in `main.py`. + + This is the engine we will use for the tests. + + We use the new URL of the database for tests: + + ``` + sqlite:///testing.db + ``` + + And again, we use the connection argument `check_same_thread=False`. + +3. Then we call: + + ```Python + SQLModel.metadata.create_all(engine) + ``` + + ...to make sure we create all the tables in the new testing database. + + The **table models** are registered in `SQLModel.metadata` just because we imported *something* from `.main`, and the code in `.main` was executed, creating the classes for the **table models** and automatically registering them in `SQLModel.metadata`. + + So, by the point we call this method, the **table models** are already registered there. 💯 + +4. Here's where we create the custom **session** object for this test in a `with` block. + + It uses the new custom **engine** we created, so anything that uses this session will be using the testing database. + +5. Now, back to the dependency override, it is just returning the same **session** object from outside, that's it, that's the whole trick. + +6. By this point, the testing **session** `with` block finishes, and the session is closed, the file is closed, etc. diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_004.md b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_004.md new file mode 100644 index 0000000000..92cbe77441 --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_004.md @@ -0,0 +1,26 @@ +1. Import `StaticPool` from `sqlmodel`, we will use it in a bit. + +2. For the **SQLite URL**, don't write any file name, leave it empty. + + So, instead of: + + ``` + sqlite:///testing.db + ``` + + ...just write: + + ``` + sqlite:// + ``` + + This is enough to tell **SQLModel** (actually SQLAlchemy) that we want to use an **in-memory SQLite database**. + +3. Remember that we told the **low-level** library in charge of communicating with SQLite that we want to be able to **access the database from different threads** with `check_same_thread=False`? + + Now that we use an **in-memory database**, we need to also tell SQLAlchemy that we want to be able to use the **same in-memory database** object from different threads. + + We tell it that with the `poolclass=StaticPool` parameter. + + !!! info + You can read more details in the SQLAlchemy documentation about Using a Memory Database in Multiple Threads diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_005.md b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_005.md new file mode 100644 index 0000000000..126e1f1790 --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_005.md @@ -0,0 +1,41 @@ +1. Import `pytest`. + +2. Use the `@pytest.fixture()` decorator on top of the function to tell pytest that this is a **fixture** function (equivalent to a FastAPI dependency). + + We also give it a name of `"session"`, this will be important in the testing function. + +3. Create the fixture function. This is equivalent to a FastAPI dependency function. + + In this fixture we create the custom **engine**, with the in-memory database, we create the tables, and we create the **session**. + + Then we `yield` the `session` object. + +4. The thing that we `return` or `yield` is what will be available to the test function, in this case, the `session` object. + + Here we use `yield` so that **pytest** comes back to execute "the rest of the code" in this function once the testing function is done. + + We don't have any more visible "rest of the code" after the `yield`, but we have the end of the `with` block that will close the **session**. + + By using `yield`, pytest will: + + * run the first part + * create the **session** object + * give it to the test function + * run the test function + * once the test function is done, it will continue here, right after the `yield`, and will correctly close the **session** object in the end of the `with` block. + +5. Now, in the test function, to tell **pytest** that this test wants to get the fixture, instead of declaring something like in FastAPI with: + + ```Python + session: Session = Depends(session_fixture) + ``` + + ...the way we tell pytest what is the fixture that we want is by using the **exact same name** of the fixture. + + In this case, we named it `session`, so the parameter has to be exactly named `session` for it to work. + + We also add the type annotation `session: Session` so that we can get autocompletion and inline error checks in our editor. + +6. Now in the dependency override function, we just return the same `session` object that came from outside it. + + The `session` object comes from the parameter passed to the test function, and we just re-use it and return it here in the dependency override. diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_006.md b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_006.md new file mode 100644 index 0000000000..d44a3b67da --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_006.md @@ -0,0 +1,23 @@ +1. Create the new fixture named `"client"`. + +2. This **client fixture**, in turn, also requires the **session fixture**. + +3. Now we create the **dependency override** inside the client fixture. + +4. Set the **dependency override** in the `app.dependency_overrides` dictionary. + +5. Create the `TestClient` with the **FastAPI** `app`. + +6. `yield` the `TestClient` instance. + + By using `yield`, after the test function is done, pytest will come back to execute the rest of the code after `yield`. + +7. This is the cleanup code, after `yield`, and after the test function is done. + + Here we clear the dependency overrides (here it's only one) in the FastAPI `app`. + +8. Now the test function requires the **client fixture**. + + And inside the test function, the code is quite **simple**, we just use the `TestClient` to make requests to the API, check the data, and that's it. + + The fixtures take care of all the **setup** and **cleanup** code. diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/main.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/main.py new file mode 100644 index 0000000000..e8615d91df --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/main.py @@ -0,0 +1,104 @@ +from fastapi import Depends, FastAPI, HTTPException, Query +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class HeroBase(SQLModel): + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +class Hero(HeroBase, table=True): + id: int | None = Field(default=None, primary_key=True) + + +class HeroCreate(HeroBase): + pass + + +class HeroRead(HeroBase): + id: int + + +class HeroUpdate(SQLModel): + name: str | None = None + secret_name: str | None = None + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def get_session(): + with Session(engine) as session: + yield session + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroRead) +def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): + db_hero = Hero.from_orm(hero) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroRead]) +def read_heroes( + *, + session: Session = Depends(get_session), + offset: int = 0, + limit: int = Query(default=100, le=100), +): + heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() + return heroes + + +@app.get("/heroes/{hero_id}", response_model=HeroRead) +def read_hero(*, session: Session = Depends(get_session), hero_id: int): + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + return hero + + +@app.patch("/heroes/{hero_id}", response_model=HeroRead) +def update_hero( + *, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate +): + db_hero = session.get(Hero, hero_id) + if not db_hero: + raise HTTPException(status_code=404, detail="Hero not found") + hero_data = hero.dict(exclude_unset=True) + for key, value in hero_data.items(): + setattr(db_hero, key, value) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.delete("/heroes/{hero_id}") +def delete_hero(*, session: Session = Depends(get_session), hero_id: int): + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + session.delete(hero) + session.commit() + return {"ok": True} diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_extra_coverage.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_extra_coverage.py new file mode 100644 index 0000000000..1d8153ab9f --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_extra_coverage.py @@ -0,0 +1,38 @@ +from fastapi.testclient import TestClient +from sqlalchemy import Inspector, inspect +from sqlmodel import Session, create_engine + +from . import main as app_mod +from .test_main import client_fixture, session_fixture + +assert client_fixture, "This keeps the client fixture used below" +assert session_fixture, "This keeps the session fixture used by client_fixture" + + +def test_startup(): + app_mod.engine = create_engine("sqlite://") + app_mod.on_startup() + insp: Inspector = inspect(app_mod.engine) + assert insp.has_table(str(app_mod.Hero.__tablename__)) + + +def test_get_session(): + app_mod.engine = create_engine("sqlite://") + for session in app_mod.get_session(): + assert isinstance(session, Session) + assert session.bind == app_mod.engine + + +def test_read_hero_not_found(client: TestClient): + response = client.get("/heroes/9000") + assert response.status_code == 404 + + +def test_update_hero_not_found(client: TestClient): + response = client.patch("/heroes/9000", json={"name": "Very-Rusty-Man"}) + assert response.status_code == 404 + + +def test_delete_hero_not_found(client: TestClient): + response = client.delete("/heroes/9000") + assert response.status_code == 404 diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main.py new file mode 100644 index 0000000000..435787c79b --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main.py @@ -0,0 +1,125 @@ +import pytest +from fastapi.testclient import TestClient +from sqlmodel import Session, SQLModel, create_engine +from sqlmodel.pool import StaticPool + +from .main import Hero, app, get_session + + +@pytest.fixture(name="session") +def session_fixture(): + engine = create_engine( + "sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool + ) + SQLModel.metadata.create_all(engine) + with Session(engine) as session: + yield session + + +@pytest.fixture(name="client") +def client_fixture(session: Session): + def get_session_override(): + return session + + app.dependency_overrides[get_session] = get_session_override + client = TestClient(app) + yield client + app.dependency_overrides.clear() + + +def test_create_hero(client: TestClient): + response = client.post( + "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} + ) + data = response.json() + + assert response.status_code == 200 + assert data["name"] == "Deadpond" + assert data["secret_name"] == "Dive Wilson" + assert data["age"] is None + assert data["id"] is not None + + +def test_create_hero_incomplete(client: TestClient): + # No secret_name + response = client.post("/heroes/", json={"name": "Deadpond"}) + assert response.status_code == 422 + + +def test_create_hero_invalid(client: TestClient): + # secret_name has an invalid type + response = client.post( + "/heroes/", + json={ + "name": "Deadpond", + "secret_name": {"message": "Do you wanna know my secret identity?"}, + }, + ) + assert response.status_code == 422 + + +def test_read_heroes(session: Session, client: TestClient): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + session.add(hero_1) + session.add(hero_2) + session.commit() + + response = client.get("/heroes/") + data = response.json() + + assert response.status_code == 200 + + assert len(data) == 2 + assert data[0]["name"] == hero_1.name + assert data[0]["secret_name"] == hero_1.secret_name + assert data[0]["age"] == hero_1.age + assert data[0]["id"] == hero_1.id + assert data[1]["name"] == hero_2.name + assert data[1]["secret_name"] == hero_2.secret_name + assert data[1]["age"] == hero_2.age + assert data[1]["id"] == hero_2.id + + +def test_read_hero(session: Session, client: TestClient): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + session.add(hero_1) + session.commit() + + response = client.get(f"/heroes/{hero_1.id}") + data = response.json() + + assert response.status_code == 200 + assert data["name"] == hero_1.name + assert data["secret_name"] == hero_1.secret_name + assert data["age"] == hero_1.age + assert data["id"] == hero_1.id + + +def test_update_hero(session: Session, client: TestClient): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + session.add(hero_1) + session.commit() + + response = client.patch(f"/heroes/{hero_1.id}", json={"name": "Deadpuddle"}) + data = response.json() + + assert response.status_code == 200 + assert data["name"] == "Deadpuddle" + assert data["secret_name"] == "Dive Wilson" + assert data["age"] is None + assert data["id"] == hero_1.id + + +def test_delete_hero(session: Session, client: TestClient): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + session.add(hero_1) + session.commit() + + response = client.delete(f"/heroes/{hero_1.id}") + + hero_in_db = session.get(Hero, hero_1.id) + + assert response.status_code == 200 + + assert hero_in_db is None diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_001.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_001.py new file mode 100644 index 0000000000..3ae40773f9 --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_001.py @@ -0,0 +1,32 @@ +from fastapi.testclient import TestClient +from sqlmodel import Session, SQLModel, create_engine + +from .main import app, get_session # (1)! + + +def test_create_hero(): + engine = create_engine( + "sqlite:///testing.db", connect_args={"check_same_thread": False} + ) + SQLModel.metadata.create_all(engine) + + with Session(engine) as session: + + def get_session_override(): + return session + + app.dependency_overrides[get_session] = get_session_override + + client = TestClient(app) # (2)! + + response = client.post( # (3)! + "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} + ) + app.dependency_overrides.clear() + data = response.json() # (4)! + + assert response.status_code == 200 # (5)! + assert data["name"] == "Deadpond" # (6)! + assert data["secret_name"] == "Dive Wilson" # (7)! + assert data["age"] is None # (8)! + assert data["id"] is not None # (9)! diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_002.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_002.py new file mode 100644 index 0000000000..727580b68f --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_002.py @@ -0,0 +1,32 @@ +from fastapi.testclient import TestClient +from sqlmodel import Session, SQLModel, create_engine + +from .main import app, get_session # (1)! + + +def test_create_hero(): + engine = create_engine( + "sqlite:///testing.db", connect_args={"check_same_thread": False} + ) + SQLModel.metadata.create_all(engine) + + with Session(engine) as session: + + def get_session_override(): # (2)! + return session # (3)! + + app.dependency_overrides[get_session] = get_session_override # (4)! + + client = TestClient(app) + + response = client.post( + "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} + ) + app.dependency_overrides.clear() # (5)! + data = response.json() + + assert response.status_code == 200 + assert data["name"] == "Deadpond" + assert data["secret_name"] == "Dive Wilson" + assert data["age"] is None + assert data["id"] is not None diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_003.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_003.py new file mode 100644 index 0000000000..465c525108 --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_003.py @@ -0,0 +1,33 @@ +from fastapi.testclient import TestClient +from sqlmodel import Session, SQLModel, create_engine + +from .main import app, get_session # (1)! + + +def test_create_hero(): + engine = create_engine( # (2)! + "sqlite:///testing.db", connect_args={"check_same_thread": False} + ) + SQLModel.metadata.create_all(engine) # (3)! + + with Session(engine) as session: # (4)! + + def get_session_override(): + return session # (5)! + + app.dependency_overrides[get_session] = get_session_override # (4)! + + client = TestClient(app) + + response = client.post( + "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} + ) + app.dependency_overrides.clear() + data = response.json() + + assert response.status_code == 200 + assert data["name"] == "Deadpond" + assert data["secret_name"] == "Dive Wilson" + assert data["age"] is None + assert data["id"] is not None + # (6)! diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_004.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_004.py new file mode 100644 index 0000000000..b770a9aa59 --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_004.py @@ -0,0 +1,35 @@ +from fastapi.testclient import TestClient +from sqlmodel import Session, SQLModel, create_engine +from sqlmodel.pool import StaticPool # (1)! + +from .main import app, get_session + + +def test_create_hero(): + engine = create_engine( + "sqlite://", # (2)! + connect_args={"check_same_thread": False}, + poolclass=StaticPool, # (3)! + ) + SQLModel.metadata.create_all(engine) + + with Session(engine) as session: + + def get_session_override(): + return session + + app.dependency_overrides[get_session] = get_session_override + + client = TestClient(app) + + response = client.post( + "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} + ) + app.dependency_overrides.clear() + data = response.json() + + assert response.status_code == 200 + assert data["name"] == "Deadpond" + assert data["secret_name"] == "Dive Wilson" + assert data["age"] is None + assert data["id"] is not None diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_005.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_005.py new file mode 100644 index 0000000000..f653eef7ec --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_005.py @@ -0,0 +1,37 @@ +import pytest # (1)! +from fastapi.testclient import TestClient +from sqlmodel import Session, SQLModel, create_engine +from sqlmodel.pool import StaticPool + +from .main import app, get_session + + +@pytest.fixture(name="session") # (2)! +def session_fixture(): # (3)! + engine = create_engine( + "sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool + ) + SQLModel.metadata.create_all(engine) + with Session(engine) as session: + yield session # (4)! + + +def test_create_hero(session: Session): # (5)! + def get_session_override(): + return session # (6)! + + app.dependency_overrides[get_session] = get_session_override + + client = TestClient(app) + + response = client.post( + "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} + ) + app.dependency_overrides.clear() + data = response.json() + + assert response.status_code == 200 + assert data["name"] == "Deadpond" + assert data["secret_name"] == "Dive Wilson" + assert data["age"] is None + assert data["id"] is not None diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_006.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_006.py new file mode 100644 index 0000000000..8dbfd45caf --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_006.py @@ -0,0 +1,41 @@ +import pytest +from fastapi.testclient import TestClient +from sqlmodel import Session, SQLModel, create_engine +from sqlmodel.pool import StaticPool + +from .main import app, get_session + + +@pytest.fixture(name="session") +def session_fixture(): + engine = create_engine( + "sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool + ) + SQLModel.metadata.create_all(engine) + with Session(engine) as session: + yield session + + +@pytest.fixture(name="client") # (1)! +def client_fixture(session: Session): # (2)! + def get_session_override(): # (3)! + return session + + app.dependency_overrides[get_session] = get_session_override # (4)! + + client = TestClient(app) # (5)! + yield client # (6)! + app.dependency_overrides.clear() # (7)! + + +def test_create_hero(client: TestClient): # (8)! + response = client.post( + "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} + ) + data = response.json() + + assert response.status_code == 200 + assert data["name"] == "Deadpond" + assert data["secret_name"] == "Dive Wilson" + assert data["age"] is None + assert data["id"] is not None diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/__init__.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_001.md b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_001.md new file mode 100644 index 0000000000..936b84b92d --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_001.md @@ -0,0 +1,17 @@ +1. Import the `app` from the the `main` module. + +2. We create a `TestClient` for the FastAPI `app` and put it in the variable `client`. + +3. Then we use use this `client` to **talk to the API** and send a `POST` HTTP operation, creating a new hero. + +4. Then we get the **JSON data** from the response and put it in the variable `data`. + +5. Next we start testing the results with `assert` statements, we check that the status code of the response is `200`. + +6. We check that the `name` of the hero created is `"Deadpond"`. + +7. We check that the `secret_name` of the hero created is `"Dive Wilson"`. + +8. We check that the `age` of the hero created is `None`, because we didn't send an age. + +9. We check that the hero created has an `id` created by the database, so it's not `None`. diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_002.md b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_002.md new file mode 100644 index 0000000000..0f8555a8dd --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_002.md @@ -0,0 +1,25 @@ +1. Import the `get_session` dependency from the the `main` module. + +2. Define the new function that will be the new **dependency override**. + +3. This function will return a different **session** than the one that would be returned by the original `get_session` function. + + We haven't seen how this new **session** object is created yet, but the point is that this is a different session than the original one from the app. + + This session is attached to a different **engine**, and that different **engine** uses a different URL, for a database just for testing. + + We haven't defined that new **URL** nor the new **engine** yet, but here we already see the that this object `session` will override the one returned by the original dependency `get_session()`. + +4. Then, the FastAPI `app` object has an attribute `app.dependency_overrides`. + + This attribute is a dictionary, and we can put dependency overrides in it by passing, as the **key**, the **original dependency function**, and as the **value**, the **new overriding dependency function**. + + So, here we are telling the FastAPI app to use `get_session_override` instead of `get_session` in all the places in the code that depend on `get_session`, that is, all the parameters with something like: + + ```Python + session: Session = Depends(get_session) + ``` + +5. After we are done with the dependency override, we can restore the application back to normal, by removing all the values in this dictionary `app.dependency_overrides`. + + This way whenever a *path operation function* needs the dependency FastAPI will use the original one instead of the override. diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_003.md b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_003.md new file mode 100644 index 0000000000..2b48ebdacf --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_003.md @@ -0,0 +1,37 @@ +1. Here's a subtle thing to notice. + + Remember that [Order Matters](../create-db-and-table.md#sqlmodel-metadata-order-matters){.internal-link target=_blank} and we need to make sure all the **SQLModel** models are already defined and **imported** before calling `.create_all()`. + + IN this line, by importing something, *anything*, from `.main`, the code in `.main` will be executed, including the definition of the **table models**, and that will automatically register them in `SQLModel.metadata`. + +2. Here we create a new **engine**, completely different from the one in `main.py`. + + This is the engine we will use for the tests. + + We use the new URL of the database for tests: + + ``` + sqlite:///testing.db + ``` + + And again, we use the connection argument `check_same_thread=False`. + +3. Then we call: + + ```Python + SQLModel.metadata.create_all(engine) + ``` + + ...to make sure we create all the tables in the new testing database. + + The **table models** are registered in `SQLModel.metadata` just because we imported *something* from `.main`, and the code in `.main` was executed, creating the classes for the **table models** and automatically registering them in `SQLModel.metadata`. + + So, by the point we call this method, the **table models** are already registered there. 💯 + +4. Here's where we create the custom **session** object for this test in a `with` block. + + It uses the new custom **engine** we created, so anything that uses this session will be using the testing database. + +5. Now, back to the dependency override, it is just returning the same **session** object from outside, that's it, that's the whole trick. + +6. By this point, the testing **session** `with` block finishes, and the session is closed, the file is closed, etc. diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_004.md b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_004.md new file mode 100644 index 0000000000..92cbe77441 --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_004.md @@ -0,0 +1,26 @@ +1. Import `StaticPool` from `sqlmodel`, we will use it in a bit. + +2. For the **SQLite URL**, don't write any file name, leave it empty. + + So, instead of: + + ``` + sqlite:///testing.db + ``` + + ...just write: + + ``` + sqlite:// + ``` + + This is enough to tell **SQLModel** (actually SQLAlchemy) that we want to use an **in-memory SQLite database**. + +3. Remember that we told the **low-level** library in charge of communicating with SQLite that we want to be able to **access the database from different threads** with `check_same_thread=False`? + + Now that we use an **in-memory database**, we need to also tell SQLAlchemy that we want to be able to use the **same in-memory database** object from different threads. + + We tell it that with the `poolclass=StaticPool` parameter. + + !!! info + You can read more details in the SQLAlchemy documentation about Using a Memory Database in Multiple Threads diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_005.md b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_005.md new file mode 100644 index 0000000000..126e1f1790 --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_005.md @@ -0,0 +1,41 @@ +1. Import `pytest`. + +2. Use the `@pytest.fixture()` decorator on top of the function to tell pytest that this is a **fixture** function (equivalent to a FastAPI dependency). + + We also give it a name of `"session"`, this will be important in the testing function. + +3. Create the fixture function. This is equivalent to a FastAPI dependency function. + + In this fixture we create the custom **engine**, with the in-memory database, we create the tables, and we create the **session**. + + Then we `yield` the `session` object. + +4. The thing that we `return` or `yield` is what will be available to the test function, in this case, the `session` object. + + Here we use `yield` so that **pytest** comes back to execute "the rest of the code" in this function once the testing function is done. + + We don't have any more visible "rest of the code" after the `yield`, but we have the end of the `with` block that will close the **session**. + + By using `yield`, pytest will: + + * run the first part + * create the **session** object + * give it to the test function + * run the test function + * once the test function is done, it will continue here, right after the `yield`, and will correctly close the **session** object in the end of the `with` block. + +5. Now, in the test function, to tell **pytest** that this test wants to get the fixture, instead of declaring something like in FastAPI with: + + ```Python + session: Session = Depends(session_fixture) + ``` + + ...the way we tell pytest what is the fixture that we want is by using the **exact same name** of the fixture. + + In this case, we named it `session`, so the parameter has to be exactly named `session` for it to work. + + We also add the type annotation `session: Session` so that we can get autocompletion and inline error checks in our editor. + +6. Now in the dependency override function, we just return the same `session` object that came from outside it. + + The `session` object comes from the parameter passed to the test function, and we just re-use it and return it here in the dependency override. diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_006.md b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_006.md new file mode 100644 index 0000000000..d44a3b67da --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_006.md @@ -0,0 +1,23 @@ +1. Create the new fixture named `"client"`. + +2. This **client fixture**, in turn, also requires the **session fixture**. + +3. Now we create the **dependency override** inside the client fixture. + +4. Set the **dependency override** in the `app.dependency_overrides` dictionary. + +5. Create the `TestClient` with the **FastAPI** `app`. + +6. `yield` the `TestClient` instance. + + By using `yield`, after the test function is done, pytest will come back to execute the rest of the code after `yield`. + +7. This is the cleanup code, after `yield`, and after the test function is done. + + Here we clear the dependency overrides (here it's only one) in the FastAPI `app`. + +8. Now the test function requires the **client fixture**. + + And inside the test function, the code is quite **simple**, we just use the `TestClient` to make requests to the API, check the data, and that's it. + + The fixtures take care of all the **setup** and **cleanup** code. diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/main.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/main.py new file mode 100644 index 0000000000..9816e70eb0 --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/main.py @@ -0,0 +1,106 @@ +from typing import Optional + +from fastapi import Depends, FastAPI, HTTPException, Query +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class HeroBase(SQLModel): + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + +class Hero(HeroBase, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + + +class HeroCreate(HeroBase): + pass + + +class HeroRead(HeroBase): + id: int + + +class HeroUpdate(SQLModel): + name: Optional[str] = None + secret_name: Optional[str] = None + age: Optional[int] = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def get_session(): + with Session(engine) as session: + yield session + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroRead) +def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): + db_hero = Hero.from_orm(hero) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroRead]) +def read_heroes( + *, + session: Session = Depends(get_session), + offset: int = 0, + limit: int = Query(default=100, le=100), +): + heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() + return heroes + + +@app.get("/heroes/{hero_id}", response_model=HeroRead) +def read_hero(*, session: Session = Depends(get_session), hero_id: int): + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + return hero + + +@app.patch("/heroes/{hero_id}", response_model=HeroRead) +def update_hero( + *, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate +): + db_hero = session.get(Hero, hero_id) + if not db_hero: + raise HTTPException(status_code=404, detail="Hero not found") + hero_data = hero.dict(exclude_unset=True) + for key, value in hero_data.items(): + setattr(db_hero, key, value) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.delete("/heroes/{hero_id}") +def delete_hero(*, session: Session = Depends(get_session), hero_id: int): + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + session.delete(hero) + session.commit() + return {"ok": True} diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_extra_coverage.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_extra_coverage.py new file mode 100644 index 0000000000..1d8153ab9f --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_extra_coverage.py @@ -0,0 +1,38 @@ +from fastapi.testclient import TestClient +from sqlalchemy import Inspector, inspect +from sqlmodel import Session, create_engine + +from . import main as app_mod +from .test_main import client_fixture, session_fixture + +assert client_fixture, "This keeps the client fixture used below" +assert session_fixture, "This keeps the session fixture used by client_fixture" + + +def test_startup(): + app_mod.engine = create_engine("sqlite://") + app_mod.on_startup() + insp: Inspector = inspect(app_mod.engine) + assert insp.has_table(str(app_mod.Hero.__tablename__)) + + +def test_get_session(): + app_mod.engine = create_engine("sqlite://") + for session in app_mod.get_session(): + assert isinstance(session, Session) + assert session.bind == app_mod.engine + + +def test_read_hero_not_found(client: TestClient): + response = client.get("/heroes/9000") + assert response.status_code == 404 + + +def test_update_hero_not_found(client: TestClient): + response = client.patch("/heroes/9000", json={"name": "Very-Rusty-Man"}) + assert response.status_code == 404 + + +def test_delete_hero_not_found(client: TestClient): + response = client.delete("/heroes/9000") + assert response.status_code == 404 diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main.py new file mode 100644 index 0000000000..435787c79b --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main.py @@ -0,0 +1,125 @@ +import pytest +from fastapi.testclient import TestClient +from sqlmodel import Session, SQLModel, create_engine +from sqlmodel.pool import StaticPool + +from .main import Hero, app, get_session + + +@pytest.fixture(name="session") +def session_fixture(): + engine = create_engine( + "sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool + ) + SQLModel.metadata.create_all(engine) + with Session(engine) as session: + yield session + + +@pytest.fixture(name="client") +def client_fixture(session: Session): + def get_session_override(): + return session + + app.dependency_overrides[get_session] = get_session_override + client = TestClient(app) + yield client + app.dependency_overrides.clear() + + +def test_create_hero(client: TestClient): + response = client.post( + "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} + ) + data = response.json() + + assert response.status_code == 200 + assert data["name"] == "Deadpond" + assert data["secret_name"] == "Dive Wilson" + assert data["age"] is None + assert data["id"] is not None + + +def test_create_hero_incomplete(client: TestClient): + # No secret_name + response = client.post("/heroes/", json={"name": "Deadpond"}) + assert response.status_code == 422 + + +def test_create_hero_invalid(client: TestClient): + # secret_name has an invalid type + response = client.post( + "/heroes/", + json={ + "name": "Deadpond", + "secret_name": {"message": "Do you wanna know my secret identity?"}, + }, + ) + assert response.status_code == 422 + + +def test_read_heroes(session: Session, client: TestClient): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + session.add(hero_1) + session.add(hero_2) + session.commit() + + response = client.get("/heroes/") + data = response.json() + + assert response.status_code == 200 + + assert len(data) == 2 + assert data[0]["name"] == hero_1.name + assert data[0]["secret_name"] == hero_1.secret_name + assert data[0]["age"] == hero_1.age + assert data[0]["id"] == hero_1.id + assert data[1]["name"] == hero_2.name + assert data[1]["secret_name"] == hero_2.secret_name + assert data[1]["age"] == hero_2.age + assert data[1]["id"] == hero_2.id + + +def test_read_hero(session: Session, client: TestClient): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + session.add(hero_1) + session.commit() + + response = client.get(f"/heroes/{hero_1.id}") + data = response.json() + + assert response.status_code == 200 + assert data["name"] == hero_1.name + assert data["secret_name"] == hero_1.secret_name + assert data["age"] == hero_1.age + assert data["id"] == hero_1.id + + +def test_update_hero(session: Session, client: TestClient): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + session.add(hero_1) + session.commit() + + response = client.patch(f"/heroes/{hero_1.id}", json={"name": "Deadpuddle"}) + data = response.json() + + assert response.status_code == 200 + assert data["name"] == "Deadpuddle" + assert data["secret_name"] == "Dive Wilson" + assert data["age"] is None + assert data["id"] == hero_1.id + + +def test_delete_hero(session: Session, client: TestClient): + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + session.add(hero_1) + session.commit() + + response = client.delete(f"/heroes/{hero_1.id}") + + hero_in_db = session.get(Hero, hero_1.id) + + assert response.status_code == 200 + + assert hero_in_db is None diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_001.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_001.py new file mode 100644 index 0000000000..3ae40773f9 --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_001.py @@ -0,0 +1,32 @@ +from fastapi.testclient import TestClient +from sqlmodel import Session, SQLModel, create_engine + +from .main import app, get_session # (1)! + + +def test_create_hero(): + engine = create_engine( + "sqlite:///testing.db", connect_args={"check_same_thread": False} + ) + SQLModel.metadata.create_all(engine) + + with Session(engine) as session: + + def get_session_override(): + return session + + app.dependency_overrides[get_session] = get_session_override + + client = TestClient(app) # (2)! + + response = client.post( # (3)! + "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} + ) + app.dependency_overrides.clear() + data = response.json() # (4)! + + assert response.status_code == 200 # (5)! + assert data["name"] == "Deadpond" # (6)! + assert data["secret_name"] == "Dive Wilson" # (7)! + assert data["age"] is None # (8)! + assert data["id"] is not None # (9)! diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_002.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_002.py new file mode 100644 index 0000000000..727580b68f --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_002.py @@ -0,0 +1,32 @@ +from fastapi.testclient import TestClient +from sqlmodel import Session, SQLModel, create_engine + +from .main import app, get_session # (1)! + + +def test_create_hero(): + engine = create_engine( + "sqlite:///testing.db", connect_args={"check_same_thread": False} + ) + SQLModel.metadata.create_all(engine) + + with Session(engine) as session: + + def get_session_override(): # (2)! + return session # (3)! + + app.dependency_overrides[get_session] = get_session_override # (4)! + + client = TestClient(app) + + response = client.post( + "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} + ) + app.dependency_overrides.clear() # (5)! + data = response.json() + + assert response.status_code == 200 + assert data["name"] == "Deadpond" + assert data["secret_name"] == "Dive Wilson" + assert data["age"] is None + assert data["id"] is not None diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_003.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_003.py new file mode 100644 index 0000000000..465c525108 --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_003.py @@ -0,0 +1,33 @@ +from fastapi.testclient import TestClient +from sqlmodel import Session, SQLModel, create_engine + +from .main import app, get_session # (1)! + + +def test_create_hero(): + engine = create_engine( # (2)! + "sqlite:///testing.db", connect_args={"check_same_thread": False} + ) + SQLModel.metadata.create_all(engine) # (3)! + + with Session(engine) as session: # (4)! + + def get_session_override(): + return session # (5)! + + app.dependency_overrides[get_session] = get_session_override # (4)! + + client = TestClient(app) + + response = client.post( + "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} + ) + app.dependency_overrides.clear() + data = response.json() + + assert response.status_code == 200 + assert data["name"] == "Deadpond" + assert data["secret_name"] == "Dive Wilson" + assert data["age"] is None + assert data["id"] is not None + # (6)! diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_004.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_004.py new file mode 100644 index 0000000000..b770a9aa59 --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_004.py @@ -0,0 +1,35 @@ +from fastapi.testclient import TestClient +from sqlmodel import Session, SQLModel, create_engine +from sqlmodel.pool import StaticPool # (1)! + +from .main import app, get_session + + +def test_create_hero(): + engine = create_engine( + "sqlite://", # (2)! + connect_args={"check_same_thread": False}, + poolclass=StaticPool, # (3)! + ) + SQLModel.metadata.create_all(engine) + + with Session(engine) as session: + + def get_session_override(): + return session + + app.dependency_overrides[get_session] = get_session_override + + client = TestClient(app) + + response = client.post( + "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} + ) + app.dependency_overrides.clear() + data = response.json() + + assert response.status_code == 200 + assert data["name"] == "Deadpond" + assert data["secret_name"] == "Dive Wilson" + assert data["age"] is None + assert data["id"] is not None diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_005.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_005.py new file mode 100644 index 0000000000..f653eef7ec --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_005.py @@ -0,0 +1,37 @@ +import pytest # (1)! +from fastapi.testclient import TestClient +from sqlmodel import Session, SQLModel, create_engine +from sqlmodel.pool import StaticPool + +from .main import app, get_session + + +@pytest.fixture(name="session") # (2)! +def session_fixture(): # (3)! + engine = create_engine( + "sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool + ) + SQLModel.metadata.create_all(engine) + with Session(engine) as session: + yield session # (4)! + + +def test_create_hero(session: Session): # (5)! + def get_session_override(): + return session # (6)! + + app.dependency_overrides[get_session] = get_session_override + + client = TestClient(app) + + response = client.post( + "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} + ) + app.dependency_overrides.clear() + data = response.json() + + assert response.status_code == 200 + assert data["name"] == "Deadpond" + assert data["secret_name"] == "Dive Wilson" + assert data["age"] is None + assert data["id"] is not None diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_006.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_006.py new file mode 100644 index 0000000000..8dbfd45caf --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/test_main_006.py @@ -0,0 +1,41 @@ +import pytest +from fastapi.testclient import TestClient +from sqlmodel import Session, SQLModel, create_engine +from sqlmodel.pool import StaticPool + +from .main import app, get_session + + +@pytest.fixture(name="session") +def session_fixture(): + engine = create_engine( + "sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool + ) + SQLModel.metadata.create_all(engine) + with Session(engine) as session: + yield session + + +@pytest.fixture(name="client") # (1)! +def client_fixture(session: Session): # (2)! + def get_session_override(): # (3)! + return session + + app.dependency_overrides[get_session] = get_session_override # (4)! + + client = TestClient(app) # (5)! + yield client # (6)! + app.dependency_overrides.clear() # (7)! + + +def test_create_hero(client: TestClient): # (8)! + response = client.post( + "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} + ) + data = response.json() + + assert response.status_code == 200 + assert data["name"] == "Deadpond" + assert data["secret_name"] == "Dive Wilson" + assert data["age"] is None + assert data["id"] is not None diff --git a/docs_src/tutorial/fastapi/delete/tutorial001_py310.py b/docs_src/tutorial/fastapi/delete/tutorial001_py310.py new file mode 100644 index 0000000000..5b2da0a0b1 --- /dev/null +++ b/docs_src/tutorial/fastapi/delete/tutorial001_py310.py @@ -0,0 +1,97 @@ +from fastapi import FastAPI, HTTPException, Query +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class HeroBase(SQLModel): + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +class Hero(HeroBase, table=True): + id: int | None = Field(default=None, primary_key=True) + + +class HeroCreate(HeroBase): + pass + + +class HeroRead(HeroBase): + id: int + + +class HeroUpdate(SQLModel): + name: str | None = None + secret_name: str | None = None + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroRead) +def create_hero(hero: HeroCreate): + with Session(engine) as session: + db_hero = Hero.from_orm(hero) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroRead]) +def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): + with Session(engine) as session: + heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() + return heroes + + +@app.get("/heroes/{hero_id}", response_model=HeroRead) +def read_hero(hero_id: int): + with Session(engine) as session: + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + return hero + + +@app.patch("/heroes/{hero_id}", response_model=HeroRead) +def update_hero(hero_id: int, hero: HeroUpdate): + with Session(engine) as session: + db_hero = session.get(Hero, hero_id) + if not db_hero: + raise HTTPException(status_code=404, detail="Hero not found") + hero_data = hero.dict(exclude_unset=True) + for key, value in hero_data.items(): + setattr(db_hero, key, value) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.delete("/heroes/{hero_id}") +def delete_hero(hero_id: int): + with Session(engine) as session: + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + session.delete(hero) + session.commit() + return {"ok": True} diff --git a/docs_src/tutorial/fastapi/delete/tutorial001_py39.py b/docs_src/tutorial/fastapi/delete/tutorial001_py39.py new file mode 100644 index 0000000000..5f498cf136 --- /dev/null +++ b/docs_src/tutorial/fastapi/delete/tutorial001_py39.py @@ -0,0 +1,99 @@ +from typing import Optional + +from fastapi import FastAPI, HTTPException, Query +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class HeroBase(SQLModel): + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + +class Hero(HeroBase, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + + +class HeroCreate(HeroBase): + pass + + +class HeroRead(HeroBase): + id: int + + +class HeroUpdate(SQLModel): + name: Optional[str] = None + secret_name: Optional[str] = None + age: Optional[int] = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroRead) +def create_hero(hero: HeroCreate): + with Session(engine) as session: + db_hero = Hero.from_orm(hero) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroRead]) +def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): + with Session(engine) as session: + heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() + return heroes + + +@app.get("/heroes/{hero_id}", response_model=HeroRead) +def read_hero(hero_id: int): + with Session(engine) as session: + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + return hero + + +@app.patch("/heroes/{hero_id}", response_model=HeroRead) +def update_hero(hero_id: int, hero: HeroUpdate): + with Session(engine) as session: + db_hero = session.get(Hero, hero_id) + if not db_hero: + raise HTTPException(status_code=404, detail="Hero not found") + hero_data = hero.dict(exclude_unset=True) + for key, value in hero_data.items(): + setattr(db_hero, key, value) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.delete("/heroes/{hero_id}") +def delete_hero(hero_id: int): + with Session(engine) as session: + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + session.delete(hero) + session.commit() + return {"ok": True} diff --git a/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py310.py b/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py310.py new file mode 100644 index 0000000000..874a6e8438 --- /dev/null +++ b/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py310.py @@ -0,0 +1,65 @@ +from fastapi import FastAPI, HTTPException, Query +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class HeroBase(SQLModel): + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +class Hero(HeroBase, table=True): + id: int | None = Field(default=None, primary_key=True) + + +class HeroCreate(HeroBase): + pass + + +class HeroRead(HeroBase): + id: int + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroRead) +def create_hero(hero: HeroCreate): + with Session(engine) as session: + db_hero = Hero.from_orm(hero) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroRead]) +def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): + with Session(engine) as session: + heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() + return heroes + + +@app.get("/heroes/{hero_id}", response_model=HeroRead) +def read_hero(hero_id: int): + with Session(engine) as session: + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + return hero diff --git a/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py39.py b/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py39.py new file mode 100644 index 0000000000..b63fa753ff --- /dev/null +++ b/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py39.py @@ -0,0 +1,67 @@ +from typing import Optional + +from fastapi import FastAPI, HTTPException, Query +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class HeroBase(SQLModel): + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + +class Hero(HeroBase, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + + +class HeroCreate(HeroBase): + pass + + +class HeroRead(HeroBase): + id: int + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroRead) +def create_hero(hero: HeroCreate): + with Session(engine) as session: + db_hero = Hero.from_orm(hero) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroRead]) +def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): + with Session(engine) as session: + heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() + return heroes + + +@app.get("/heroes/{hero_id}", response_model=HeroRead) +def read_hero(hero_id: int): + with Session(engine) as session: + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + return hero diff --git a/docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py b/docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py new file mode 100644 index 0000000000..13129f383f --- /dev/null +++ b/docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py @@ -0,0 +1,58 @@ +from fastapi import FastAPI +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +class HeroCreate(SQLModel): + name: str + secret_name: str + age: int | None = None + + +class HeroRead(SQLModel): + id: int + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroRead) +def create_hero(hero: HeroCreate): + with Session(engine) as session: + db_hero = Hero.from_orm(hero) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroRead]) +def read_heroes(): + with Session(engine) as session: + heroes = session.exec(select(Hero)).all() + return heroes diff --git a/docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py b/docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py new file mode 100644 index 0000000000..41a51f448d --- /dev/null +++ b/docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py @@ -0,0 +1,60 @@ +from typing import Optional + +from fastapi import FastAPI +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + +class HeroCreate(SQLModel): + name: str + secret_name: str + age: Optional[int] = None + + +class HeroRead(SQLModel): + id: int + name: str + secret_name: str + age: Optional[int] = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroRead) +def create_hero(hero: HeroCreate): + with Session(engine) as session: + db_hero = Hero.from_orm(hero) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroRead]) +def read_heroes(): + with Session(engine) as session: + heroes = session.exec(select(Hero)).all() + return heroes diff --git a/docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py b/docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py new file mode 100644 index 0000000000..3eda88b194 --- /dev/null +++ b/docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py @@ -0,0 +1,56 @@ +from fastapi import FastAPI +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class HeroBase(SQLModel): + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +class Hero(HeroBase, table=True): + id: int | None = Field(default=None, primary_key=True) + + +class HeroCreate(HeroBase): + pass + + +class HeroRead(HeroBase): + id: int + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroRead) +def create_hero(hero: HeroCreate): + with Session(engine) as session: + db_hero = Hero.from_orm(hero) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroRead]) +def read_heroes(): + with Session(engine) as session: + heroes = session.exec(select(Hero)).all() + return heroes diff --git a/docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py b/docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py new file mode 100644 index 0000000000..473fe5b832 --- /dev/null +++ b/docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py @@ -0,0 +1,58 @@ +from typing import Optional + +from fastapi import FastAPI +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class HeroBase(SQLModel): + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + +class Hero(HeroBase, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + + +class HeroCreate(HeroBase): + pass + + +class HeroRead(HeroBase): + id: int + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroRead) +def create_hero(hero: HeroCreate): + with Session(engine) as session: + db_hero = Hero.from_orm(hero) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroRead]) +def read_heroes(): + with Session(engine) as session: + heroes = session.exec(select(Hero)).all() + return heroes diff --git a/docs_src/tutorial/fastapi/read_one/tutorial001_py310.py b/docs_src/tutorial/fastapi/read_one/tutorial001_py310.py new file mode 100644 index 0000000000..8883570dc5 --- /dev/null +++ b/docs_src/tutorial/fastapi/read_one/tutorial001_py310.py @@ -0,0 +1,65 @@ +from fastapi import FastAPI, HTTPException +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class HeroBase(SQLModel): + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +class Hero(HeroBase, table=True): + id: int | None = Field(default=None, primary_key=True) + + +class HeroCreate(HeroBase): + pass + + +class HeroRead(HeroBase): + id: int + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroRead) +def create_hero(hero: HeroCreate): + with Session(engine) as session: + db_hero = Hero.from_orm(hero) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroRead]) +def read_heroes(): + with Session(engine) as session: + heroes = session.exec(select(Hero)).all() + return heroes + + +@app.get("/heroes/{hero_id}", response_model=HeroRead) +def read_hero(hero_id: int): + with Session(engine) as session: + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + return hero diff --git a/docs_src/tutorial/fastapi/read_one/tutorial001_py39.py b/docs_src/tutorial/fastapi/read_one/tutorial001_py39.py new file mode 100644 index 0000000000..0ad7016687 --- /dev/null +++ b/docs_src/tutorial/fastapi/read_one/tutorial001_py39.py @@ -0,0 +1,67 @@ +from typing import Optional + +from fastapi import FastAPI, HTTPException +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class HeroBase(SQLModel): + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + +class Hero(HeroBase, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + + +class HeroCreate(HeroBase): + pass + + +class HeroRead(HeroBase): + id: int + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroRead) +def create_hero(hero: HeroCreate): + with Session(engine) as session: + db_hero = Hero.from_orm(hero) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroRead]) +def read_heroes(): + with Session(engine) as session: + heroes = session.exec(select(Hero)).all() + return heroes + + +@app.get("/heroes/{hero_id}", response_model=HeroRead) +def read_hero(hero_id: int): + with Session(engine) as session: + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + return hero diff --git a/docs_src/tutorial/fastapi/relationships/tutorial001_py310.py b/docs_src/tutorial/fastapi/relationships/tutorial001_py310.py new file mode 100644 index 0000000000..bec6a6f2e2 --- /dev/null +++ b/docs_src/tutorial/fastapi/relationships/tutorial001_py310.py @@ -0,0 +1,199 @@ +from fastapi import Depends, FastAPI, HTTPException, Query +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class TeamBase(SQLModel): + name: str = Field(index=True) + headquarters: str + + +class Team(TeamBase, table=True): + id: int | None = Field(default=None, primary_key=True) + + heroes: list["Hero"] = Relationship(back_populates="team") + + +class TeamCreate(TeamBase): + pass + + +class TeamRead(TeamBase): + id: int + + +class TeamUpdate(SQLModel): + id: int | None = None + name: str | None = None + headquarters: str | None = None + + +class HeroBase(SQLModel): + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + + +class Hero(HeroBase, table=True): + id: int | None = Field(default=None, primary_key=True) + + team: Team | None = Relationship(back_populates="heroes") + + +class HeroRead(HeroBase): + id: int + + +class HeroCreate(HeroBase): + pass + + +class HeroUpdate(SQLModel): + name: str | None = None + secret_name: str | None = None + age: int | None = None + team_id: int | None = None + + +class HeroReadWithTeam(HeroRead): + team: TeamRead | None = None + + +class TeamReadWithHeroes(TeamRead): + heroes: list[HeroRead] = [] + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def get_session(): + with Session(engine) as session: + yield session + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroRead) +def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): + db_hero = Hero.from_orm(hero) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroRead]) +def read_heroes( + *, + session: Session = Depends(get_session), + offset: int = 0, + limit: int = Query(default=100, le=100), +): + heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() + return heroes + + +@app.get("/heroes/{hero_id}", response_model=HeroReadWithTeam) +def read_hero(*, session: Session = Depends(get_session), hero_id: int): + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + return hero + + +@app.patch("/heroes/{hero_id}", response_model=HeroRead) +def update_hero( + *, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate +): + db_hero = session.get(Hero, hero_id) + if not db_hero: + raise HTTPException(status_code=404, detail="Hero not found") + hero_data = hero.dict(exclude_unset=True) + for key, value in hero_data.items(): + setattr(db_hero, key, value) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.delete("/heroes/{hero_id}") +def delete_hero(*, session: Session = Depends(get_session), hero_id: int): + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + session.delete(hero) + session.commit() + return {"ok": True} + + +@app.post("/teams/", response_model=TeamRead) +def create_team(*, session: Session = Depends(get_session), team: TeamCreate): + db_team = Team.from_orm(team) + session.add(db_team) + session.commit() + session.refresh(db_team) + return db_team + + +@app.get("/teams/", response_model=list[TeamRead]) +def read_teams( + *, + session: Session = Depends(get_session), + offset: int = 0, + limit: int = Query(default=100, le=100), +): + teams = session.exec(select(Team).offset(offset).limit(limit)).all() + return teams + + +@app.get("/teams/{team_id}", response_model=TeamReadWithHeroes) +def read_team(*, team_id: int, session: Session = Depends(get_session)): + team = session.get(Team, team_id) + if not team: + raise HTTPException(status_code=404, detail="Team not found") + return team + + +@app.patch("/teams/{team_id}", response_model=TeamRead) +def update_team( + *, + session: Session = Depends(get_session), + team_id: int, + team: TeamUpdate, +): + db_team = session.get(Team, team_id) + if not db_team: + raise HTTPException(status_code=404, detail="Team not found") + team_data = team.dict(exclude_unset=True) + for key, value in team_data.items(): + setattr(db_team, key, value) + session.add(db_team) + session.commit() + session.refresh(db_team) + return db_team + + +@app.delete("/teams/{team_id}") +def delete_team(*, session: Session = Depends(get_session), team_id: int): + team = session.get(Team, team_id) + if not team: + raise HTTPException(status_code=404, detail="Team not found") + session.delete(team) + session.commit() + return {"ok": True} diff --git a/docs_src/tutorial/fastapi/relationships/tutorial001_py39.py b/docs_src/tutorial/fastapi/relationships/tutorial001_py39.py new file mode 100644 index 0000000000..3893905519 --- /dev/null +++ b/docs_src/tutorial/fastapi/relationships/tutorial001_py39.py @@ -0,0 +1,201 @@ +from typing import Optional + +from fastapi import Depends, FastAPI, HTTPException, Query +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class TeamBase(SQLModel): + name: str = Field(index=True) + headquarters: str + + +class Team(TeamBase, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + + heroes: list["Hero"] = Relationship(back_populates="team") + + +class TeamCreate(TeamBase): + pass + + +class TeamRead(TeamBase): + id: int + + +class TeamUpdate(SQLModel): + id: Optional[int] = None + name: Optional[str] = None + headquarters: Optional[str] = None + + +class HeroBase(SQLModel): + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + team_id: Optional[int] = Field(default=None, foreign_key="team.id") + + +class Hero(HeroBase, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + + team: Optional[Team] = Relationship(back_populates="heroes") + + +class HeroRead(HeroBase): + id: int + + +class HeroCreate(HeroBase): + pass + + +class HeroUpdate(SQLModel): + name: Optional[str] = None + secret_name: Optional[str] = None + age: Optional[int] = None + team_id: Optional[int] = None + + +class HeroReadWithTeam(HeroRead): + team: Optional[TeamRead] = None + + +class TeamReadWithHeroes(TeamRead): + heroes: list[HeroRead] = [] + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def get_session(): + with Session(engine) as session: + yield session + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroRead) +def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): + db_hero = Hero.from_orm(hero) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroRead]) +def read_heroes( + *, + session: Session = Depends(get_session), + offset: int = 0, + limit: int = Query(default=100, le=100), +): + heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() + return heroes + + +@app.get("/heroes/{hero_id}", response_model=HeroReadWithTeam) +def read_hero(*, session: Session = Depends(get_session), hero_id: int): + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + return hero + + +@app.patch("/heroes/{hero_id}", response_model=HeroRead) +def update_hero( + *, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate +): + db_hero = session.get(Hero, hero_id) + if not db_hero: + raise HTTPException(status_code=404, detail="Hero not found") + hero_data = hero.dict(exclude_unset=True) + for key, value in hero_data.items(): + setattr(db_hero, key, value) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.delete("/heroes/{hero_id}") +def delete_hero(*, session: Session = Depends(get_session), hero_id: int): + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + session.delete(hero) + session.commit() + return {"ok": True} + + +@app.post("/teams/", response_model=TeamRead) +def create_team(*, session: Session = Depends(get_session), team: TeamCreate): + db_team = Team.from_orm(team) + session.add(db_team) + session.commit() + session.refresh(db_team) + return db_team + + +@app.get("/teams/", response_model=list[TeamRead]) +def read_teams( + *, + session: Session = Depends(get_session), + offset: int = 0, + limit: int = Query(default=100, le=100), +): + teams = session.exec(select(Team).offset(offset).limit(limit)).all() + return teams + + +@app.get("/teams/{team_id}", response_model=TeamReadWithHeroes) +def read_team(*, team_id: int, session: Session = Depends(get_session)): + team = session.get(Team, team_id) + if not team: + raise HTTPException(status_code=404, detail="Team not found") + return team + + +@app.patch("/teams/{team_id}", response_model=TeamRead) +def update_team( + *, + session: Session = Depends(get_session), + team_id: int, + team: TeamUpdate, +): + db_team = session.get(Team, team_id) + if not db_team: + raise HTTPException(status_code=404, detail="Team not found") + team_data = team.dict(exclude_unset=True) + for key, value in team_data.items(): + setattr(db_team, key, value) + session.add(db_team) + session.commit() + session.refresh(db_team) + return db_team + + +@app.delete("/teams/{team_id}") +def delete_team(*, session: Session = Depends(get_session), team_id: int): + team = session.get(Team, team_id) + if not team: + raise HTTPException(status_code=404, detail="Team not found") + session.delete(team) + session.commit() + return {"ok": True} diff --git a/docs_src/tutorial/fastapi/response_model/tutorial001_py310.py b/docs_src/tutorial/fastapi/response_model/tutorial001_py310.py new file mode 100644 index 0000000000..25825b41ec --- /dev/null +++ b/docs_src/tutorial/fastapi/response_model/tutorial001_py310.py @@ -0,0 +1,44 @@ +from fastapi import FastAPI +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=Hero) +def create_hero(hero: Hero): + with Session(engine) as session: + session.add(hero) + session.commit() + session.refresh(hero) + return hero + + +@app.get("/heroes/", response_model=list[Hero]) +def read_heroes(): + with Session(engine) as session: + heroes = session.exec(select(Hero)).all() + return heroes diff --git a/docs_src/tutorial/fastapi/response_model/tutorial001_py39.py b/docs_src/tutorial/fastapi/response_model/tutorial001_py39.py new file mode 100644 index 0000000000..53b701deb1 --- /dev/null +++ b/docs_src/tutorial/fastapi/response_model/tutorial001_py39.py @@ -0,0 +1,46 @@ +from typing import Optional + +from fastapi import FastAPI +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=Hero) +def create_hero(hero: Hero): + with Session(engine) as session: + session.add(hero) + session.commit() + session.refresh(hero) + return hero + + +@app.get("/heroes/", response_model=list[Hero]) +def read_heroes(): + with Session(engine) as session: + heroes = session.exec(select(Hero)).all() + return heroes diff --git a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py new file mode 100644 index 0000000000..e8615d91df --- /dev/null +++ b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py @@ -0,0 +1,104 @@ +from fastapi import Depends, FastAPI, HTTPException, Query +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class HeroBase(SQLModel): + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +class Hero(HeroBase, table=True): + id: int | None = Field(default=None, primary_key=True) + + +class HeroCreate(HeroBase): + pass + + +class HeroRead(HeroBase): + id: int + + +class HeroUpdate(SQLModel): + name: str | None = None + secret_name: str | None = None + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def get_session(): + with Session(engine) as session: + yield session + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroRead) +def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): + db_hero = Hero.from_orm(hero) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroRead]) +def read_heroes( + *, + session: Session = Depends(get_session), + offset: int = 0, + limit: int = Query(default=100, le=100), +): + heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() + return heroes + + +@app.get("/heroes/{hero_id}", response_model=HeroRead) +def read_hero(*, session: Session = Depends(get_session), hero_id: int): + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + return hero + + +@app.patch("/heroes/{hero_id}", response_model=HeroRead) +def update_hero( + *, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate +): + db_hero = session.get(Hero, hero_id) + if not db_hero: + raise HTTPException(status_code=404, detail="Hero not found") + hero_data = hero.dict(exclude_unset=True) + for key, value in hero_data.items(): + setattr(db_hero, key, value) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.delete("/heroes/{hero_id}") +def delete_hero(*, session: Session = Depends(get_session), hero_id: int): + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + session.delete(hero) + session.commit() + return {"ok": True} diff --git a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py new file mode 100644 index 0000000000..9816e70eb0 --- /dev/null +++ b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py @@ -0,0 +1,106 @@ +from typing import Optional + +from fastapi import Depends, FastAPI, HTTPException, Query +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class HeroBase(SQLModel): + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + +class Hero(HeroBase, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + + +class HeroCreate(HeroBase): + pass + + +class HeroRead(HeroBase): + id: int + + +class HeroUpdate(SQLModel): + name: Optional[str] = None + secret_name: Optional[str] = None + age: Optional[int] = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def get_session(): + with Session(engine) as session: + yield session + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroRead) +def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): + db_hero = Hero.from_orm(hero) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroRead]) +def read_heroes( + *, + session: Session = Depends(get_session), + offset: int = 0, + limit: int = Query(default=100, le=100), +): + heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() + return heroes + + +@app.get("/heroes/{hero_id}", response_model=HeroRead) +def read_hero(*, session: Session = Depends(get_session), hero_id: int): + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + return hero + + +@app.patch("/heroes/{hero_id}", response_model=HeroRead) +def update_hero( + *, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate +): + db_hero = session.get(Hero, hero_id) + if not db_hero: + raise HTTPException(status_code=404, detail="Hero not found") + hero_data = hero.dict(exclude_unset=True) + for key, value in hero_data.items(): + setattr(db_hero, key, value) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.delete("/heroes/{hero_id}") +def delete_hero(*, session: Session = Depends(get_session), hero_id: int): + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + session.delete(hero) + session.commit() + return {"ok": True} diff --git a/docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py b/docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py new file mode 100644 index 0000000000..0e113b0f16 --- /dev/null +++ b/docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py @@ -0,0 +1,44 @@ +from fastapi import FastAPI +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/") +def create_hero(hero: Hero): + with Session(engine) as session: + session.add(hero) + session.commit() + session.refresh(hero) + return hero + + +@app.get("/heroes/") +def read_heroes(): + with Session(engine) as session: + heroes = session.exec(select(Hero)).all() + return heroes diff --git a/docs_src/tutorial/fastapi/teams/tutorial001_py310.py b/docs_src/tutorial/fastapi/teams/tutorial001_py310.py new file mode 100644 index 0000000000..a9a527df73 --- /dev/null +++ b/docs_src/tutorial/fastapi/teams/tutorial001_py310.py @@ -0,0 +1,190 @@ +from fastapi import Depends, FastAPI, HTTPException, Query +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class TeamBase(SQLModel): + name: str = Field(index=True) + headquarters: str + + +class Team(TeamBase, table=True): + id: int | None = Field(default=None, primary_key=True) + + heroes: list["Hero"] = Relationship(back_populates="team") + + +class TeamCreate(TeamBase): + pass + + +class TeamRead(TeamBase): + id: int + + +class TeamUpdate(SQLModel): + name: str | None = None + headquarters: str | None = None + + +class HeroBase(SQLModel): + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + + +class Hero(HeroBase, table=True): + id: int | None = Field(default=None, primary_key=True) + + team: Team | None = Relationship(back_populates="heroes") + + +class HeroRead(HeroBase): + id: int + + +class HeroCreate(HeroBase): + pass + + +class HeroUpdate(SQLModel): + name: str | None = None + secret_name: str | None = None + age: int | None = None + team_id: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def get_session(): + with Session(engine) as session: + yield session + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroRead) +def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): + db_hero = Hero.from_orm(hero) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroRead]) +def read_heroes( + *, + session: Session = Depends(get_session), + offset: int = 0, + limit: int = Query(default=100, le=100), +): + heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() + return heroes + + +@app.get("/heroes/{hero_id}", response_model=HeroRead) +def read_hero(*, session: Session = Depends(get_session), hero_id: int): + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + return hero + + +@app.patch("/heroes/{hero_id}", response_model=HeroRead) +def update_hero( + *, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate +): + db_hero = session.get(Hero, hero_id) + if not db_hero: + raise HTTPException(status_code=404, detail="Hero not found") + hero_data = hero.dict(exclude_unset=True) + for key, value in hero_data.items(): + setattr(db_hero, key, value) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.delete("/heroes/{hero_id}") +def delete_hero(*, session: Session = Depends(get_session), hero_id: int): + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + session.delete(hero) + session.commit() + return {"ok": True} + + +@app.post("/teams/", response_model=TeamRead) +def create_team(*, session: Session = Depends(get_session), team: TeamCreate): + db_team = Team.from_orm(team) + session.add(db_team) + session.commit() + session.refresh(db_team) + return db_team + + +@app.get("/teams/", response_model=list[TeamRead]) +def read_teams( + *, + session: Session = Depends(get_session), + offset: int = 0, + limit: int = Query(default=100, le=100), +): + teams = session.exec(select(Team).offset(offset).limit(limit)).all() + return teams + + +@app.get("/teams/{team_id}", response_model=TeamRead) +def read_team(*, team_id: int, session: Session = Depends(get_session)): + team = session.get(Team, team_id) + if not team: + raise HTTPException(status_code=404, detail="Team not found") + return team + + +@app.patch("/teams/{team_id}", response_model=TeamRead) +def update_team( + *, + session: Session = Depends(get_session), + team_id: int, + team: TeamUpdate, +): + db_team = session.get(Team, team_id) + if not db_team: + raise HTTPException(status_code=404, detail="Team not found") + team_data = team.dict(exclude_unset=True) + for key, value in team_data.items(): + setattr(db_team, key, value) + session.add(db_team) + session.commit() + session.refresh(db_team) + return db_team + + +@app.delete("/teams/{team_id}") +def delete_team(*, session: Session = Depends(get_session), team_id: int): + team = session.get(Team, team_id) + if not team: + raise HTTPException(status_code=404, detail="Team not found") + session.delete(team) + session.commit() + return {"ok": True} diff --git a/docs_src/tutorial/fastapi/teams/tutorial001_py39.py b/docs_src/tutorial/fastapi/teams/tutorial001_py39.py new file mode 100644 index 0000000000..1a36428994 --- /dev/null +++ b/docs_src/tutorial/fastapi/teams/tutorial001_py39.py @@ -0,0 +1,192 @@ +from typing import Optional + +from fastapi import Depends, FastAPI, HTTPException, Query +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class TeamBase(SQLModel): + name: str = Field(index=True) + headquarters: str + + +class Team(TeamBase, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + + heroes: list["Hero"] = Relationship(back_populates="team") + + +class TeamCreate(TeamBase): + pass + + +class TeamRead(TeamBase): + id: int + + +class TeamUpdate(SQLModel): + name: Optional[str] = None + headquarters: Optional[str] = None + + +class HeroBase(SQLModel): + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + team_id: Optional[int] = Field(default=None, foreign_key="team.id") + + +class Hero(HeroBase, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + + team: Optional[Team] = Relationship(back_populates="heroes") + + +class HeroRead(HeroBase): + id: int + + +class HeroCreate(HeroBase): + pass + + +class HeroUpdate(SQLModel): + name: Optional[str] = None + secret_name: Optional[str] = None + age: Optional[int] = None + team_id: Optional[int] = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def get_session(): + with Session(engine) as session: + yield session + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroRead) +def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): + db_hero = Hero.from_orm(hero) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroRead]) +def read_heroes( + *, + session: Session = Depends(get_session), + offset: int = 0, + limit: int = Query(default=100, le=100), +): + heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() + return heroes + + +@app.get("/heroes/{hero_id}", response_model=HeroRead) +def read_hero(*, session: Session = Depends(get_session), hero_id: int): + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + return hero + + +@app.patch("/heroes/{hero_id}", response_model=HeroRead) +def update_hero( + *, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate +): + db_hero = session.get(Hero, hero_id) + if not db_hero: + raise HTTPException(status_code=404, detail="Hero not found") + hero_data = hero.dict(exclude_unset=True) + for key, value in hero_data.items(): + setattr(db_hero, key, value) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.delete("/heroes/{hero_id}") +def delete_hero(*, session: Session = Depends(get_session), hero_id: int): + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + session.delete(hero) + session.commit() + return {"ok": True} + + +@app.post("/teams/", response_model=TeamRead) +def create_team(*, session: Session = Depends(get_session), team: TeamCreate): + db_team = Team.from_orm(team) + session.add(db_team) + session.commit() + session.refresh(db_team) + return db_team + + +@app.get("/teams/", response_model=list[TeamRead]) +def read_teams( + *, + session: Session = Depends(get_session), + offset: int = 0, + limit: int = Query(default=100, le=100), +): + teams = session.exec(select(Team).offset(offset).limit(limit)).all() + return teams + + +@app.get("/teams/{team_id}", response_model=TeamRead) +def read_team(*, team_id: int, session: Session = Depends(get_session)): + team = session.get(Team, team_id) + if not team: + raise HTTPException(status_code=404, detail="Team not found") + return team + + +@app.patch("/teams/{team_id}", response_model=TeamRead) +def update_team( + *, + session: Session = Depends(get_session), + team_id: int, + team: TeamUpdate, +): + db_team = session.get(Team, team_id) + if not db_team: + raise HTTPException(status_code=404, detail="Team not found") + team_data = team.dict(exclude_unset=True) + for key, value in team_data.items(): + setattr(db_team, key, value) + session.add(db_team) + session.commit() + session.refresh(db_team) + return db_team + + +@app.delete("/teams/{team_id}") +def delete_team(*, session: Session = Depends(get_session), team_id: int): + team = session.get(Team, team_id) + if not team: + raise HTTPException(status_code=404, detail="Team not found") + session.delete(team) + session.commit() + return {"ok": True} diff --git a/docs_src/tutorial/fastapi/update/tutorial001_py310.py b/docs_src/tutorial/fastapi/update/tutorial001_py310.py new file mode 100644 index 0000000000..79069181fb --- /dev/null +++ b/docs_src/tutorial/fastapi/update/tutorial001_py310.py @@ -0,0 +1,86 @@ +from fastapi import FastAPI, HTTPException, Query +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class HeroBase(SQLModel): + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +class Hero(HeroBase, table=True): + id: int | None = Field(default=None, primary_key=True) + + +class HeroCreate(HeroBase): + pass + + +class HeroRead(HeroBase): + id: int + + +class HeroUpdate(SQLModel): + name: str | None = None + secret_name: str | None = None + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroRead) +def create_hero(hero: HeroCreate): + with Session(engine) as session: + db_hero = Hero.from_orm(hero) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroRead]) +def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): + with Session(engine) as session: + heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() + return heroes + + +@app.get("/heroes/{hero_id}", response_model=HeroRead) +def read_hero(hero_id: int): + with Session(engine) as session: + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + return hero + + +@app.patch("/heroes/{hero_id}", response_model=HeroRead) +def update_hero(hero_id: int, hero: HeroUpdate): + with Session(engine) as session: + db_hero = session.get(Hero, hero_id) + if not db_hero: + raise HTTPException(status_code=404, detail="Hero not found") + hero_data = hero.dict(exclude_unset=True) + for key, value in hero_data.items(): + setattr(db_hero, key, value) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero diff --git a/docs_src/tutorial/fastapi/update/tutorial001_py39.py b/docs_src/tutorial/fastapi/update/tutorial001_py39.py new file mode 100644 index 0000000000..c788eb1c7a --- /dev/null +++ b/docs_src/tutorial/fastapi/update/tutorial001_py39.py @@ -0,0 +1,88 @@ +from typing import Optional + +from fastapi import FastAPI, HTTPException, Query +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class HeroBase(SQLModel): + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + +class Hero(HeroBase, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + + +class HeroCreate(HeroBase): + pass + + +class HeroRead(HeroBase): + id: int + + +class HeroUpdate(SQLModel): + name: Optional[str] = None + secret_name: Optional[str] = None + age: Optional[int] = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroRead) +def create_hero(hero: HeroCreate): + with Session(engine) as session: + db_hero = Hero.from_orm(hero) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroRead]) +def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): + with Session(engine) as session: + heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() + return heroes + + +@app.get("/heroes/{hero_id}", response_model=HeroRead) +def read_hero(hero_id: int): + with Session(engine) as session: + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + return hero + + +@app.patch("/heroes/{hero_id}", response_model=HeroRead) +def update_hero(hero_id: int, hero: HeroUpdate): + with Session(engine) as session: + db_hero = session.get(Hero, hero_id) + if not db_hero: + raise HTTPException(status_code=404, detail="Hero not found") + hero_data = hero.dict(exclude_unset=True) + for key, value in hero_data.items(): + setattr(db_hero, key, value) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero diff --git a/docs_src/tutorial/indexes/tutorial001_py310.py b/docs_src/tutorial/indexes/tutorial001_py310.py new file mode 100644 index 0000000000..115c447bcf --- /dev/null +++ b/docs_src/tutorial/indexes/tutorial001_py310.py @@ -0,0 +1,49 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Deadpond") + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/indexes/tutorial002_py310.py b/docs_src/tutorial/indexes/tutorial002_py310.py new file mode 100644 index 0000000000..c0b7a1e65e --- /dev/null +++ b/docs_src/tutorial/indexes/tutorial002_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.age <= 35) + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/insert/tutorial001_py310.py b/docs_src/tutorial/insert/tutorial001_py310.py new file mode 100644 index 0000000000..72f95ee279 --- /dev/null +++ b/docs_src/tutorial/insert/tutorial001_py310.py @@ -0,0 +1,43 @@ +from sqlmodel import Field, Session, SQLModel, create_engine + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + + session = Session(engine) + + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + + session.commit() + + session.close() + + +def main(): + create_db_and_tables() + create_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/insert/tutorial002_py310.py b/docs_src/tutorial/insert/tutorial002_py310.py new file mode 100644 index 0000000000..266fbb8d32 --- /dev/null +++ b/docs_src/tutorial/insert/tutorial002_py310.py @@ -0,0 +1,40 @@ +from sqlmodel import Field, Session, SQLModel, create_engine + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + + session.commit() + + +def main(): + create_db_and_tables() + create_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/insert/tutorial003_py310.py b/docs_src/tutorial/insert/tutorial003_py310.py new file mode 100644 index 0000000000..da94841aba --- /dev/null +++ b/docs_src/tutorial/insert/tutorial003_py310.py @@ -0,0 +1,41 @@ +from sqlmodel import Field, Session, SQLModel, create_engine + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): # (1)! + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") # (2)! + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + + with Session(engine) as session: # (3)! + session.add(hero_1) # (4)! + session.add(hero_2) + session.add(hero_3) + + session.commit() # (5)! + # (6)! + + +def main(): # (7)! + create_db_and_tables() # (8)! + create_heroes() # (9)! + + +if __name__ == "__main__": # (10)! + main() # (11)! diff --git a/docs_src/tutorial/many_to_many/tutorial001_py310.py b/docs_src/tutorial/many_to_many/tutorial001_py310.py new file mode 100644 index 0000000000..5e8f31caa0 --- /dev/null +++ b/docs_src/tutorial/many_to_many/tutorial001_py310.py @@ -0,0 +1,78 @@ +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine + + +class HeroTeamLink(SQLModel, table=True): + team_id: int | None = Field(default=None, foreign_key="team.id", primary_key=True) + hero_id: int | None = Field(default=None, foreign_key="hero.id", primary_key=True) + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="teams", link_model=HeroTeamLink) + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + teams: list[Team] = Relationship(back_populates="heroes", link_model=HeroTeamLink) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + + hero_deadpond = Hero( + name="Deadpond", + secret_name="Dive Wilson", + teams=[team_z_force, team_preventers], + ) + hero_rusty_man = Hero( + name="Rusty-Man", + secret_name="Tommy Sharp", + age=48, + teams=[team_preventers], + ) + hero_spider_boy = Hero( + name="Spider-Boy", secret_name="Pedro Parqueador", teams=[team_preventers] + ) + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Deadpond:", hero_deadpond) + print("Deadpond teams:", hero_deadpond.teams) + print("Rusty-Man:", hero_rusty_man) + print("Rusty-Man Teams:", hero_rusty_man.teams) + print("Spider-Boy:", hero_spider_boy) + print("Spider-Boy Teams:", hero_spider_boy.teams) + + +def main(): + create_db_and_tables() + create_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/many_to_many/tutorial001_py39.py b/docs_src/tutorial/many_to_many/tutorial001_py39.py new file mode 100644 index 0000000000..0d7325a723 --- /dev/null +++ b/docs_src/tutorial/many_to_many/tutorial001_py39.py @@ -0,0 +1,84 @@ +from typing import Optional + +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine + + +class HeroTeamLink(SQLModel, table=True): + team_id: Optional[int] = Field( + default=None, foreign_key="team.id", primary_key=True + ) + hero_id: Optional[int] = Field( + default=None, foreign_key="hero.id", primary_key=True + ) + + +class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="teams", link_model=HeroTeamLink) + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + teams: list[Team] = Relationship(back_populates="heroes", link_model=HeroTeamLink) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + + hero_deadpond = Hero( + name="Deadpond", + secret_name="Dive Wilson", + teams=[team_z_force, team_preventers], + ) + hero_rusty_man = Hero( + name="Rusty-Man", + secret_name="Tommy Sharp", + age=48, + teams=[team_preventers], + ) + hero_spider_boy = Hero( + name="Spider-Boy", secret_name="Pedro Parqueador", teams=[team_preventers] + ) + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Deadpond:", hero_deadpond) + print("Deadpond teams:", hero_deadpond.teams) + print("Rusty-Man:", hero_rusty_man) + print("Rusty-Man Teams:", hero_rusty_man.teams) + print("Spider-Boy:", hero_spider_boy) + print("Spider-Boy Teams:", hero_spider_boy.teams) + + +def main(): + create_db_and_tables() + create_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/many_to_many/tutorial002_py310.py b/docs_src/tutorial/many_to_many/tutorial002_py310.py new file mode 100644 index 0000000000..5823a6e72e --- /dev/null +++ b/docs_src/tutorial/many_to_many/tutorial002_py310.py @@ -0,0 +1,101 @@ +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class HeroTeamLink(SQLModel, table=True): + team_id: int | None = Field(default=None, foreign_key="team.id", primary_key=True) + hero_id: int | None = Field(default=None, foreign_key="hero.id", primary_key=True) + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="teams", link_model=HeroTeamLink) + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + teams: list[Team] = Relationship(back_populates="heroes", link_model=HeroTeamLink) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + + hero_deadpond = Hero( + name="Deadpond", + secret_name="Dive Wilson", + teams=[team_z_force, team_preventers], + ) + hero_rusty_man = Hero( + name="Rusty-Man", + secret_name="Tommy Sharp", + age=48, + teams=[team_preventers], + ) + hero_spider_boy = Hero( + name="Spider-Boy", secret_name="Pedro Parqueador", teams=[team_preventers] + ) + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Deadpond:", hero_deadpond) + print("Deadpond teams:", hero_deadpond.teams) + print("Rusty-Man:", hero_rusty_man) + print("Rusty-Man Teams:", hero_rusty_man.teams) + print("Spider-Boy:", hero_spider_boy) + print("Spider-Boy Teams:", hero_spider_boy.teams) + + +def update_heroes(): + with Session(engine) as session: + hero_spider_boy = session.exec( + select(Hero).where(Hero.name == "Spider-Boy") + ).one() + team_z_force = session.exec(select(Team).where(Team.name == "Z-Force")).one() + + team_z_force.heroes.append(hero_spider_boy) + session.add(team_z_force) + session.commit() + + print("Updated Spider-Boy's Teams:", hero_spider_boy.teams) + print("Z-Force heroes:", team_z_force.heroes) + + hero_spider_boy.teams.remove(team_z_force) + session.add(team_z_force) + session.commit() + + print("Reverted Z-Force's heroes:", team_z_force.heroes) + print("Reverted Spider-Boy's teams:", hero_spider_boy.teams) + + +def main(): + create_db_and_tables() + create_heroes() + update_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/many_to_many/tutorial002_py39.py b/docs_src/tutorial/many_to_many/tutorial002_py39.py new file mode 100644 index 0000000000..54c5d45356 --- /dev/null +++ b/docs_src/tutorial/many_to_many/tutorial002_py39.py @@ -0,0 +1,107 @@ +from typing import Optional + +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class HeroTeamLink(SQLModel, table=True): + team_id: Optional[int] = Field( + default=None, foreign_key="team.id", primary_key=True + ) + hero_id: Optional[int] = Field( + default=None, foreign_key="hero.id", primary_key=True + ) + + +class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="teams", link_model=HeroTeamLink) + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + teams: list[Team] = Relationship(back_populates="heroes", link_model=HeroTeamLink) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + + hero_deadpond = Hero( + name="Deadpond", + secret_name="Dive Wilson", + teams=[team_z_force, team_preventers], + ) + hero_rusty_man = Hero( + name="Rusty-Man", + secret_name="Tommy Sharp", + age=48, + teams=[team_preventers], + ) + hero_spider_boy = Hero( + name="Spider-Boy", secret_name="Pedro Parqueador", teams=[team_preventers] + ) + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + session.refresh(hero_spider_boy) + + print("Deadpond:", hero_deadpond) + print("Deadpond teams:", hero_deadpond.teams) + print("Rusty-Man:", hero_rusty_man) + print("Rusty-Man Teams:", hero_rusty_man.teams) + print("Spider-Boy:", hero_spider_boy) + print("Spider-Boy Teams:", hero_spider_boy.teams) + + +def update_heroes(): + with Session(engine) as session: + hero_spider_boy = session.exec( + select(Hero).where(Hero.name == "Spider-Boy") + ).one() + team_z_force = session.exec(select(Team).where(Team.name == "Z-Force")).one() + + team_z_force.heroes.append(hero_spider_boy) + session.add(team_z_force) + session.commit() + + print("Updated Spider-Boy's Teams:", hero_spider_boy.teams) + print("Z-Force heroes:", team_z_force.heroes) + + hero_spider_boy.teams.remove(team_z_force) + session.add(team_z_force) + session.commit() + + print("Reverted Z-Force's heroes:", team_z_force.heroes) + print("Reverted Spider-Boy's teams:", hero_spider_boy.teams) + + +def main(): + create_db_and_tables() + create_heroes() + update_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/many_to_many/tutorial003_py310.py b/docs_src/tutorial/many_to_many/tutorial003_py310.py new file mode 100644 index 0000000000..b8fa4632fe --- /dev/null +++ b/docs_src/tutorial/many_to_many/tutorial003_py310.py @@ -0,0 +1,117 @@ +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class HeroTeamLink(SQLModel, table=True): + team_id: int | None = Field(default=None, foreign_key="team.id", primary_key=True) + hero_id: int | None = Field(default=None, foreign_key="hero.id", primary_key=True) + is_training: bool = False + + team: "Team" = Relationship(back_populates="hero_links") + hero: "Hero" = Relationship(back_populates="team_links") + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + hero_links: list[HeroTeamLink] = Relationship(back_populates="team") + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_links: list[HeroTeamLink] = Relationship(back_populates="hero") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + + hero_deadpond = Hero( + name="Deadpond", + secret_name="Dive Wilson", + ) + hero_rusty_man = Hero( + name="Rusty-Man", + secret_name="Tommy Sharp", + age=48, + ) + hero_spider_boy = Hero( + name="Spider-Boy", + secret_name="Pedro Parqueador", + ) + deadpond_team_z_link = HeroTeamLink(team=team_z_force, hero=hero_deadpond) + deadpond_preventers_link = HeroTeamLink( + team=team_preventers, hero=hero_deadpond, is_training=True + ) + spider_boy_preventers_link = HeroTeamLink( + team=team_preventers, hero=hero_spider_boy, is_training=True + ) + rusty_man_preventers_link = HeroTeamLink( + team=team_preventers, hero=hero_rusty_man + ) + + session.add(deadpond_team_z_link) + session.add(deadpond_preventers_link) + session.add(spider_boy_preventers_link) + session.add(rusty_man_preventers_link) + session.commit() + + for link in team_z_force.hero_links: + print("Z-Force hero:", link.hero, "is training:", link.is_training) + + for link in team_preventers.hero_links: + print("Preventers hero:", link.hero, "is training:", link.is_training) + + +def update_heroes(): + with Session(engine) as session: + hero_spider_boy = session.exec( + select(Hero).where(Hero.name == "Spider-Boy") + ).one() + team_z_force = session.exec(select(Team).where(Team.name == "Z-Force")).one() + + spider_boy_z_force_link = HeroTeamLink( + team=team_z_force, hero=hero_spider_boy, is_training=True + ) + team_z_force.hero_links.append(spider_boy_z_force_link) + session.add(team_z_force) + session.commit() + + print("Updated Spider-Boy's Teams:", hero_spider_boy.team_links) + print("Z-Force heroes:", team_z_force.hero_links) + + for link in hero_spider_boy.team_links: + if link.team.name == "Preventers": + link.is_training = False + + session.add(hero_spider_boy) + session.commit() + + for link in hero_spider_boy.team_links: + print("Spider-Boy team:", link.team, "is training:", link.is_training) + + +def main(): + create_db_and_tables() + create_heroes() + update_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/many_to_many/tutorial003_py39.py b/docs_src/tutorial/many_to_many/tutorial003_py39.py new file mode 100644 index 0000000000..214228a122 --- /dev/null +++ b/docs_src/tutorial/many_to_many/tutorial003_py39.py @@ -0,0 +1,123 @@ +from typing import Optional + +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class HeroTeamLink(SQLModel, table=True): + team_id: Optional[int] = Field( + default=None, foreign_key="team.id", primary_key=True + ) + hero_id: Optional[int] = Field( + default=None, foreign_key="hero.id", primary_key=True + ) + is_training: bool = False + + team: "Team" = Relationship(back_populates="hero_links") + hero: "Hero" = Relationship(back_populates="team_links") + + +class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + hero_links: list[HeroTeamLink] = Relationship(back_populates="team") + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + team_links: list[HeroTeamLink] = Relationship(back_populates="hero") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + + hero_deadpond = Hero( + name="Deadpond", + secret_name="Dive Wilson", + ) + hero_rusty_man = Hero( + name="Rusty-Man", + secret_name="Tommy Sharp", + age=48, + ) + hero_spider_boy = Hero( + name="Spider-Boy", + secret_name="Pedro Parqueador", + ) + deadpond_team_z_link = HeroTeamLink(team=team_z_force, hero=hero_deadpond) + deadpond_preventers_link = HeroTeamLink( + team=team_preventers, hero=hero_deadpond, is_training=True + ) + spider_boy_preventers_link = HeroTeamLink( + team=team_preventers, hero=hero_spider_boy, is_training=True + ) + rusty_man_preventers_link = HeroTeamLink( + team=team_preventers, hero=hero_rusty_man + ) + + session.add(deadpond_team_z_link) + session.add(deadpond_preventers_link) + session.add(spider_boy_preventers_link) + session.add(rusty_man_preventers_link) + session.commit() + + for link in team_z_force.hero_links: + print("Z-Force hero:", link.hero, "is training:", link.is_training) + + for link in team_preventers.hero_links: + print("Preventers hero:", link.hero, "is training:", link.is_training) + + +def update_heroes(): + with Session(engine) as session: + hero_spider_boy = session.exec( + select(Hero).where(Hero.name == "Spider-Boy") + ).one() + team_z_force = session.exec(select(Team).where(Team.name == "Z-Force")).one() + + spider_boy_z_force_link = HeroTeamLink( + team=team_z_force, hero=hero_spider_boy, is_training=True + ) + team_z_force.hero_links.append(spider_boy_z_force_link) + session.add(team_z_force) + session.commit() + + print("Updated Spider-Boy's Teams:", hero_spider_boy.team_links) + print("Z-Force heroes:", team_z_force.hero_links) + + for link in hero_spider_boy.team_links: + if link.team.name == "Preventers": + link.is_training = False + + session.add(hero_spider_boy) + session.commit() + + for link in hero_spider_boy.team_links: + print("Spider-Boy team:", link.team, "is training:", link.is_training) + + +def main(): + create_db_and_tables() + create_heroes() + update_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/offset_and_limit/tutorial001_py310.py b/docs_src/tutorial/offset_and_limit/tutorial001_py310.py new file mode 100644 index 0000000000..931f46e247 --- /dev/null +++ b/docs_src/tutorial/offset_and_limit/tutorial001_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).limit(3) + results = session.exec(statement) + heroes = results.all() + print(heroes) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/offset_and_limit/tutorial002_py310.py b/docs_src/tutorial/offset_and_limit/tutorial002_py310.py new file mode 100644 index 0000000000..ab5c89b7cc --- /dev/null +++ b/docs_src/tutorial/offset_and_limit/tutorial002_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).offset(3).limit(3) + results = session.exec(statement) + heroes = results.all() + print(heroes) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/offset_and_limit/tutorial003_py310.py b/docs_src/tutorial/offset_and_limit/tutorial003_py310.py new file mode 100644 index 0000000000..5ac24937d6 --- /dev/null +++ b/docs_src/tutorial/offset_and_limit/tutorial003_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).offset(6).limit(3) + results = session.exec(statement) + heroes = results.all() + print(heroes) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/offset_and_limit/tutorial004_py310.py b/docs_src/tutorial/offset_and_limit/tutorial004_py310.py new file mode 100644 index 0000000000..c7e7bbb414 --- /dev/null +++ b/docs_src/tutorial/offset_and_limit/tutorial004_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.age > 32).offset(1).limit(2) + results = session.exec(statement) + heroes = results.all() + print(heroes) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/one/tutorial001_py310.py b/docs_src/tutorial/one/tutorial001_py310.py new file mode 100644 index 0000000000..bb1326deef --- /dev/null +++ b/docs_src/tutorial/one/tutorial001_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.age <= 35) + results = session.exec(statement) + hero = results.first() + print("Hero:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/one/tutorial002_py310.py b/docs_src/tutorial/one/tutorial002_py310.py new file mode 100644 index 0000000000..b82fcfd845 --- /dev/null +++ b/docs_src/tutorial/one/tutorial002_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.age < 25) + results = session.exec(statement) + hero = results.first() + print("Hero:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/one/tutorial003_py310.py b/docs_src/tutorial/one/tutorial003_py310.py new file mode 100644 index 0000000000..f674c8a686 --- /dev/null +++ b/docs_src/tutorial/one/tutorial003_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Deadpond") + results = session.exec(statement) + hero = results.one() + print("Hero:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/one/tutorial004_py310.py b/docs_src/tutorial/one/tutorial004_py310.py new file mode 100644 index 0000000000..e55b55304f --- /dev/null +++ b/docs_src/tutorial/one/tutorial004_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.age <= 35) + results = session.exec(statement) + hero = results.one() + print("Hero:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/one/tutorial005_py310.py b/docs_src/tutorial/one/tutorial005_py310.py new file mode 100644 index 0000000000..6c51d8fab2 --- /dev/null +++ b/docs_src/tutorial/one/tutorial005_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.age < 25) + results = session.exec(statement) + hero = results.one() + print("Hero:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/one/tutorial006_py310.py b/docs_src/tutorial/one/tutorial006_py310.py new file mode 100644 index 0000000000..6f9b7371ab --- /dev/null +++ b/docs_src/tutorial/one/tutorial006_py310.py @@ -0,0 +1,55 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + hero = session.exec(select(Hero).where(Hero.name == "Deadpond")).one() + print("Hero:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/one/tutorial007_py310.py b/docs_src/tutorial/one/tutorial007_py310.py new file mode 100644 index 0000000000..f065f5ac78 --- /dev/null +++ b/docs_src/tutorial/one/tutorial007_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.id == 1) + results = session.exec(statement) + hero = results.first() + print("Hero:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/one/tutorial008_py310.py b/docs_src/tutorial/one/tutorial008_py310.py new file mode 100644 index 0000000000..af9169704b --- /dev/null +++ b/docs_src/tutorial/one/tutorial008_py310.py @@ -0,0 +1,55 @@ +from sqlmodel import Field, Session, SQLModel, create_engine + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + hero = session.get(Hero, 1) + print("Hero:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/one/tutorial009_py310.py b/docs_src/tutorial/one/tutorial009_py310.py new file mode 100644 index 0000000000..57db99ea42 --- /dev/null +++ b/docs_src/tutorial/one/tutorial009_py310.py @@ -0,0 +1,55 @@ +from sqlmodel import Field, Session, SQLModel, create_engine + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + hero = session.get(Hero, 9001) + print("Hero:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py b/docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py new file mode 100644 index 0000000000..fb35500af8 --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py @@ -0,0 +1,141 @@ +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship() + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + team: Team | None = Relationship() + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + 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 = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + hero_tarantula = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_dr_weird = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_cap = Hero( + name="Captain North America", secret_name="Esteban Rogelios", age=93 + ) + + team_preventers.heroes.append(hero_tarantula) + team_preventers.heroes.append(hero_dr_weird) + team_preventers.heroes.append(hero_cap) + session.add(team_preventers) + session.commit() + session.refresh(hero_tarantula) + session.refresh(hero_dr_weird) + session.refresh(hero_cap) + print("Preventers new hero:", hero_tarantula) + print("Preventers new hero:", hero_dr_weird) + print("Preventers new hero:", hero_cap) + + +def select_heroes(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Preventers") + result = session.exec(statement) + team_preventers = result.one() + + print("Preventers heroes:", team_preventers.heroes) + + +def update_heroes(): + with Session(engine) as session: + hero_spider_boy = session.exec( + select(Hero).where(Hero.name == "Spider-Boy") + ).one() + + preventers_team = session.exec( + select(Team).where(Team.name == "Preventers") + ).one() + + print("Hero Spider-Boy:", hero_spider_boy) + print("Preventers Team:", preventers_team) + print("Preventers Team Heroes:", preventers_team.heroes) + + hero_spider_boy.team = None + + print("Spider-Boy without team:", hero_spider_boy) + + print("Preventers Team Heroes again:", preventers_team.heroes) + + session.add(hero_spider_boy) + session.commit() + print("After committing") + + session.refresh(hero_spider_boy) + print("Spider-Boy after commit:", hero_spider_boy) + + print("Preventers Team Heroes after commit:", preventers_team.heroes) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + update_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py b/docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py new file mode 100644 index 0000000000..a5deae91e9 --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py @@ -0,0 +1,143 @@ +from typing import Optional + +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship() + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + team_id: Optional[int] = Field(default=None, foreign_key="team.id") + team: Optional[Team] = Relationship() + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + 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 = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + hero_tarantula = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_dr_weird = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_cap = Hero( + name="Captain North America", secret_name="Esteban Rogelios", age=93 + ) + + team_preventers.heroes.append(hero_tarantula) + team_preventers.heroes.append(hero_dr_weird) + team_preventers.heroes.append(hero_cap) + session.add(team_preventers) + session.commit() + session.refresh(hero_tarantula) + session.refresh(hero_dr_weird) + session.refresh(hero_cap) + print("Preventers new hero:", hero_tarantula) + print("Preventers new hero:", hero_dr_weird) + print("Preventers new hero:", hero_cap) + + +def select_heroes(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Preventers") + result = session.exec(statement) + team_preventers = result.one() + + print("Preventers heroes:", team_preventers.heroes) + + +def update_heroes(): + with Session(engine) as session: + hero_spider_boy = session.exec( + select(Hero).where(Hero.name == "Spider-Boy") + ).one() + + preventers_team = session.exec( + select(Team).where(Team.name == "Preventers") + ).one() + + print("Hero Spider-Boy:", hero_spider_boy) + print("Preventers Team:", preventers_team) + print("Preventers Team Heroes:", preventers_team.heroes) + + hero_spider_boy.team = None + + print("Spider-Boy without team:", hero_spider_boy) + + print("Preventers Team Heroes again:", preventers_team.heroes) + + session.add(hero_spider_boy) + session.commit() + print("After committing") + + session.refresh(hero_spider_boy) + print("Spider-Boy after commit:", hero_spider_boy) + + print("Preventers Team Heroes after commit:", preventers_team.heroes) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + update_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py b/docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py new file mode 100644 index 0000000000..2113fde179 --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py @@ -0,0 +1,141 @@ +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team") + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + team: Team | None = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + 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 = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + hero_tarantula = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_dr_weird = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_cap = Hero( + name="Captain North America", secret_name="Esteban Rogelios", age=93 + ) + + team_preventers.heroes.append(hero_tarantula) + team_preventers.heroes.append(hero_dr_weird) + team_preventers.heroes.append(hero_cap) + session.add(team_preventers) + session.commit() + session.refresh(hero_tarantula) + session.refresh(hero_dr_weird) + session.refresh(hero_cap) + print("Preventers new hero:", hero_tarantula) + print("Preventers new hero:", hero_dr_weird) + print("Preventers new hero:", hero_cap) + + +def select_heroes(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Preventers") + result = session.exec(statement) + team_preventers = result.one() + + print("Preventers heroes:", team_preventers.heroes) + + +def update_heroes(): + with Session(engine) as session: + hero_spider_boy = session.exec( + select(Hero).where(Hero.name == "Spider-Boy") + ).one() + + preventers_team = session.exec( + select(Team).where(Team.name == "Preventers") + ).one() + + print("Hero Spider-Boy:", hero_spider_boy) + print("Preventers Team:", preventers_team) + print("Preventers Team Heroes:", preventers_team.heroes) + + hero_spider_boy.team = None + + print("Spider-Boy without team:", hero_spider_boy) + + print("Preventers Team Heroes again:", preventers_team.heroes) + + session.add(hero_spider_boy) + session.commit() + print("After committing") + + session.refresh(hero_spider_boy) + print("Spider-Boy after commit:", hero_spider_boy) + + print("Preventers Team Heroes after commit:", preventers_team.heroes) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + update_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py39.py b/docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py39.py new file mode 100644 index 0000000000..72a8616958 --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py39.py @@ -0,0 +1,143 @@ +from typing import Optional + +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team") + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + team_id: Optional[int] = Field(default=None, foreign_key="team.id") + team: Optional[Team] = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + 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 = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + hero_tarantula = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_dr_weird = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_cap = Hero( + name="Captain North America", secret_name="Esteban Rogelios", age=93 + ) + + team_preventers.heroes.append(hero_tarantula) + team_preventers.heroes.append(hero_dr_weird) + team_preventers.heroes.append(hero_cap) + session.add(team_preventers) + session.commit() + session.refresh(hero_tarantula) + session.refresh(hero_dr_weird) + session.refresh(hero_cap) + print("Preventers new hero:", hero_tarantula) + print("Preventers new hero:", hero_dr_weird) + print("Preventers new hero:", hero_cap) + + +def select_heroes(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Preventers") + result = session.exec(statement) + team_preventers = result.one() + + print("Preventers heroes:", team_preventers.heroes) + + +def update_heroes(): + with Session(engine) as session: + hero_spider_boy = session.exec( + select(Hero).where(Hero.name == "Spider-Boy") + ).one() + + preventers_team = session.exec( + select(Team).where(Team.name == "Preventers") + ).one() + + print("Hero Spider-Boy:", hero_spider_boy) + print("Preventers Team:", preventers_team) + print("Preventers Team Heroes:", preventers_team.heroes) + + hero_spider_boy.team = None + + print("Spider-Boy without team:", hero_spider_boy) + + print("Preventers Team Heroes again:", preventers_team.heroes) + + session.add(hero_spider_boy) + session.commit() + print("After committing") + + session.refresh(hero_spider_boy) + print("Spider-Boy after commit:", hero_spider_boy) + + print("Preventers Team Heroes after commit:", preventers_team.heroes) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + update_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/back_populates/tutorial003_py310.py b/docs_src/tutorial/relationship_attributes/back_populates/tutorial003_py310.py new file mode 100644 index 0000000000..aa850b3f69 --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/back_populates/tutorial003_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Relationship, SQLModel, create_engine + + +class Weapon(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + + hero: "Hero" = Relationship(back_populates="weapon") + + +class Power(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + + hero_id: int = Field(foreign_key="hero.id") + hero: "Hero" = Relationship(back_populates="powers") + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team") + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + team: Team | None = Relationship(back_populates="heroes") + + weapon_id: int | None = Field(default=None, foreign_key="weapon.id") + weapon: Weapon | None = Relationship(back_populates="hero") + + powers: list[Power] = Relationship(back_populates="hero") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def main(): + create_db_and_tables() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/back_populates/tutorial003_py39.py b/docs_src/tutorial/relationship_attributes/back_populates/tutorial003_py39.py new file mode 100644 index 0000000000..fc0d08df52 --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/back_populates/tutorial003_py39.py @@ -0,0 +1,59 @@ +from typing import Optional + +from sqlmodel import Field, Relationship, SQLModel, create_engine + + +class Weapon(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + + hero: "Hero" = Relationship(back_populates="weapon") + + +class Power(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + + hero_id: int = Field(foreign_key="hero.id") + hero: "Hero" = Relationship(back_populates="powers") + + +class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team") + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + team_id: Optional[int] = Field(default=None, foreign_key="team.id") + team: Optional[Team] = Relationship(back_populates="heroes") + + weapon_id: Optional[int] = Field(default=None, foreign_key="weapon.id") + weapon: Optional[Weapon] = Relationship(back_populates="hero") + + powers: list[Power] = Relationship(back_populates="hero") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def main(): + create_db_and_tables() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py b/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py new file mode 100644 index 0000000000..4e0314763e --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py @@ -0,0 +1,100 @@ +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team") + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + team: Team | None = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + 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 = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + hero_tarantula = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_dr_weird = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_cap = Hero( + name="Captain North America", secret_name="Esteban Rogelios", age=93 + ) + + team_preventers.heroes.append(hero_tarantula) + team_preventers.heroes.append(hero_dr_weird) + team_preventers.heroes.append(hero_cap) + session.add(team_preventers) + session.commit() + session.refresh(hero_tarantula) + session.refresh(hero_dr_weird) + session.refresh(hero_cap) + print("Preventers new hero:", hero_tarantula) + print("Preventers new hero:", hero_dr_weird) + print("Preventers new hero:", hero_cap) + + +def main(): + create_db_and_tables() + create_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py39.py b/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py39.py new file mode 100644 index 0000000000..bc51058a5a --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py39.py @@ -0,0 +1,102 @@ +from typing import Optional + +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine + + +class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team") + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + team_id: Optional[int] = Field(default=None, foreign_key="team.id") + team: Optional[Team] = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + 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 = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + hero_tarantula = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_dr_weird = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_cap = Hero( + name="Captain North America", secret_name="Esteban Rogelios", age=93 + ) + + team_preventers.heroes.append(hero_tarantula) + team_preventers.heroes.append(hero_dr_weird) + team_preventers.heroes.append(hero_cap) + session.add(team_preventers) + session.commit() + session.refresh(hero_tarantula) + session.refresh(hero_dr_weird) + session.refresh(hero_cap) + print("Preventers new hero:", hero_tarantula) + print("Preventers new hero:", hero_dr_weird) + print("Preventers new hero:", hero_cap) + + +def main(): + create_db_and_tables() + create_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py b/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py new file mode 100644 index 0000000000..dc4e261bb0 --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py @@ -0,0 +1,68 @@ +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team") + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + team: Team | None = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + 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 = team_preventers + session.add(hero_spider_boy) + session.commit() + + +def main(): + create_db_and_tables() + create_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py39.py b/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py39.py new file mode 100644 index 0000000000..42ba84da2a --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py39.py @@ -0,0 +1,70 @@ +from typing import Optional + +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine + + +class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team") + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + team_id: Optional[int] = Field(default=None, foreign_key="team.id") + team: Optional[Team] = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + 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 = team_preventers + session.add(hero_spider_boy) + session.commit() + + +def main(): + create_db_and_tables() + create_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py310.py b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py310.py new file mode 100644 index 0000000000..18b3f178ca --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py310.py @@ -0,0 +1,115 @@ +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team") + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + team: Team | None = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + 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 = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + hero_tarantula = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_dr_weird = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_cap = Hero( + name="Captain North America", secret_name="Esteban Rogelios", age=93 + ) + + team_preventers.heroes.append(hero_tarantula) + team_preventers.heroes.append(hero_dr_weird) + team_preventers.heroes.append(hero_cap) + session.add(team_preventers) + session.commit() + session.refresh(hero_tarantula) + session.refresh(hero_dr_weird) + session.refresh(hero_cap) + print("Preventers new hero:", hero_tarantula) + print("Preventers new hero:", hero_dr_weird) + print("Preventers new hero:", hero_cap) + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Spider-Boy") + result = session.exec(statement) + hero_spider_boy = result.one() + + statement = select(Team).where(Team.id == hero_spider_boy.team_id) + result = session.exec(statement) + team = result.first() + print("Spider-Boy's team:", team) + + print("Spider-Boy's team again:", hero_spider_boy.team) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py39.py b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py39.py new file mode 100644 index 0000000000..18c4c2edf1 --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py39.py @@ -0,0 +1,117 @@ +from typing import Optional + +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team") + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + team_id: Optional[int] = Field(default=None, foreign_key="team.id") + team: Optional[Team] = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + 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 = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + hero_tarantula = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_dr_weird = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_cap = Hero( + name="Captain North America", secret_name="Esteban Rogelios", age=93 + ) + + team_preventers.heroes.append(hero_tarantula) + team_preventers.heroes.append(hero_dr_weird) + team_preventers.heroes.append(hero_cap) + session.add(team_preventers) + session.commit() + session.refresh(hero_tarantula) + session.refresh(hero_dr_weird) + session.refresh(hero_cap) + print("Preventers new hero:", hero_tarantula) + print("Preventers new hero:", hero_dr_weird) + print("Preventers new hero:", hero_cap) + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Spider-Boy") + result = session.exec(statement) + hero_spider_boy = result.one() + + statement = select(Team).where(Team.id == hero_spider_boy.team_id) + result = session.exec(statement) + team = result.first() + print("Spider-Boy's team:", team) + + print("Spider-Boy's team again:", hero_spider_boy.team) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py310.py b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py310.py new file mode 100644 index 0000000000..f1320024ec --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py310.py @@ -0,0 +1,125 @@ +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team") + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id") + team: Team | None = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + 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 = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + hero_tarantula = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_dr_weird = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_cap = Hero( + name="Captain North America", secret_name="Esteban Rogelios", age=93 + ) + + team_preventers.heroes.append(hero_tarantula) + team_preventers.heroes.append(hero_dr_weird) + team_preventers.heroes.append(hero_cap) + session.add(team_preventers) + session.commit() + session.refresh(hero_tarantula) + session.refresh(hero_dr_weird) + session.refresh(hero_cap) + print("Preventers new hero:", hero_tarantula) + print("Preventers new hero:", hero_dr_weird) + print("Preventers new hero:", hero_cap) + + +def select_heroes(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Preventers") + result = session.exec(statement) + team_preventers = result.one() + + print("Preventers heroes:", team_preventers.heroes) + + +def update_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Spider-Boy") + result = session.exec(statement) + hero_spider_boy = result.one() + + hero_spider_boy.team = None + session.add(hero_spider_boy) + session.commit() + + session.refresh(hero_spider_boy) + print("Spider-Boy without team:", hero_spider_boy) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + update_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py39.py b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py39.py new file mode 100644 index 0000000000..33310e7cde --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py39.py @@ -0,0 +1,127 @@ +from typing import Optional + +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team") + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + team_id: Optional[int] = Field(default=None, foreign_key="team.id") + team: Optional[Team] = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + 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 = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + hero_tarantula = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_dr_weird = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_cap = Hero( + name="Captain North America", secret_name="Esteban Rogelios", age=93 + ) + + team_preventers.heroes.append(hero_tarantula) + team_preventers.heroes.append(hero_dr_weird) + team_preventers.heroes.append(hero_cap) + session.add(team_preventers) + session.commit() + session.refresh(hero_tarantula) + session.refresh(hero_dr_weird) + session.refresh(hero_cap) + print("Preventers new hero:", hero_tarantula) + print("Preventers new hero:", hero_dr_weird) + print("Preventers new hero:", hero_cap) + + +def select_heroes(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Preventers") + result = session.exec(statement) + team_preventers = result.one() + + print("Preventers heroes:", team_preventers.heroes) + + +def update_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Spider-Boy") + result = session.exec(statement) + hero_spider_boy = result.one() + + hero_spider_boy.team = None + session.add(hero_spider_boy) + session.commit() + + session.refresh(hero_spider_boy) + print("Spider-Boy without team:", hero_spider_boy) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + update_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/select/tutorial001_py310.py b/docs_src/tutorial/select/tutorial001_py310.py new file mode 100644 index 0000000000..29bce28251 --- /dev/null +++ b/docs_src/tutorial/select/tutorial001_py310.py @@ -0,0 +1,49 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero) + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/select/tutorial002_py310.py b/docs_src/tutorial/select/tutorial002_py310.py new file mode 100644 index 0000000000..b2f9d4d22c --- /dev/null +++ b/docs_src/tutorial/select/tutorial002_py310.py @@ -0,0 +1,50 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select # (1)! + + +class Hero(SQLModel, table=True): # (2)! + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) # (3)! + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) # (4)! + + +def create_heroes(): + 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) + + with Session(engine) as session: # (6)! + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: # (7)! + statement = select(Hero) # (8)! + results = session.exec(statement) # (9)! + for hero in results: # (10)! + print(hero) # (11)! + # (12)! + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() # (13)! + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/select/tutorial003_py310.py b/docs_src/tutorial/select/tutorial003_py310.py new file mode 100644 index 0000000000..836998e24e --- /dev/null +++ b/docs_src/tutorial/select/tutorial003_py310.py @@ -0,0 +1,49 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero) + results = session.exec(statement) + heroes = results.all() + print(heroes) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/select/tutorial004_py310.py b/docs_src/tutorial/select/tutorial004_py310.py new file mode 100644 index 0000000000..6366d40865 --- /dev/null +++ b/docs_src/tutorial/select/tutorial004_py310.py @@ -0,0 +1,47 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + heroes = session.exec(select(Hero)).all() + print(heroes) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/update/tutorial001_py310.py b/docs_src/tutorial/update/tutorial001_py310.py new file mode 100644 index 0000000000..6cc4f48934 --- /dev/null +++ b/docs_src/tutorial/update/tutorial001_py310.py @@ -0,0 +1,63 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def update_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Spider-Boy") + results = session.exec(statement) + hero = results.one() + print("Hero:", hero) + + hero.age = 16 + session.add(hero) + session.commit() + session.refresh(hero) + print("Updated hero:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + update_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/update/tutorial002_py310.py b/docs_src/tutorial/update/tutorial002_py310.py new file mode 100644 index 0000000000..64cb6916d8 --- /dev/null +++ b/docs_src/tutorial/update/tutorial002_py310.py @@ -0,0 +1,63 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def update_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Spider-Boy") # (1)! + results = session.exec(statement) # (2)! + hero = results.one() # (3)! + print("Hero:", hero) # (4)! + + hero.age = 16 # (5)! + session.add(hero) # (6)! + session.commit() # (7)! + session.refresh(hero) # (8)! + print("Updated hero:", hero) # (9)! + + +def main(): + create_db_and_tables() + create_heroes() + update_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/update/tutorial003_py310.py b/docs_src/tutorial/update/tutorial003_py310.py new file mode 100644 index 0000000000..f250b071c1 --- /dev/null +++ b/docs_src/tutorial/update/tutorial003_py310.py @@ -0,0 +1,77 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def update_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Spider-Boy") + results = session.exec(statement) + hero_1 = results.one() + print("Hero 1:", hero_1) + + statement = select(Hero).where(Hero.name == "Captain North America") + results = session.exec(statement) + hero_2 = results.one() + print("Hero 2:", hero_2) + + hero_1.age = 16 + hero_1.name = "Spider-Youngster" + session.add(hero_1) + + hero_2.name = "Captain North America Except Canada" + hero_2.age = 110 + session.add(hero_2) + + session.commit() + session.refresh(hero_1) + session.refresh(hero_2) + + print("Updated hero 1:", hero_1) + print("Updated hero 2:", hero_2) + + +def main(): + create_db_and_tables() + create_heroes() + update_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/update/tutorial004_py310.py b/docs_src/tutorial/update/tutorial004_py310.py new file mode 100644 index 0000000000..09e54e1cce --- /dev/null +++ b/docs_src/tutorial/update/tutorial004_py310.py @@ -0,0 +1,78 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def update_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Spider-Boy") # (1)! + results = session.exec(statement) # (2)! + hero_1 = results.one() # (3)! + print("Hero 1:", hero_1) # (4)! + + statement = select(Hero).where(Hero.name == "Captain North America") # (5)! + results = session.exec(statement) # (6)! + hero_2 = results.one() # (7)! + print("Hero 2:", hero_2) # (8)! + + hero_1.age = 16 # (9)! + hero_1.name = "Spider-Youngster" # (10)! + session.add(hero_1) # (11)! + + hero_2.name = "Captain North America Except Canada" # (12)! + hero_2.age = 110 # (13)! + session.add(hero_2) # (14)! + + session.commit() # (15)! + session.refresh(hero_1) # (16)! + session.refresh(hero_2) # (17)! + + print("Updated hero 1:", hero_1) # (18)! + print("Updated hero 2:", hero_2) # (19)! + # (20)! + + +def main(): + create_db_and_tables() + create_heroes() + update_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/where/tutorial001_py310.py b/docs_src/tutorial/where/tutorial001_py310.py new file mode 100644 index 0000000000..a59e5fc281 --- /dev/null +++ b/docs_src/tutorial/where/tutorial001_py310.py @@ -0,0 +1,49 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Deadpond") + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/where/tutorial002_py310.py b/docs_src/tutorial/where/tutorial002_py310.py new file mode 100644 index 0000000000..5db10c5808 --- /dev/null +++ b/docs_src/tutorial/where/tutorial002_py310.py @@ -0,0 +1,49 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name != "Deadpond") + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/where/tutorial003_py310.py b/docs_src/tutorial/where/tutorial003_py310.py new file mode 100644 index 0000000000..c368add499 --- /dev/null +++ b/docs_src/tutorial/where/tutorial003_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.age > 35) + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/where/tutorial004_py310.py b/docs_src/tutorial/where/tutorial004_py310.py new file mode 100644 index 0000000000..5733b71795 --- /dev/null +++ b/docs_src/tutorial/where/tutorial004_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.age >= 35) + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/where/tutorial005_py310.py b/docs_src/tutorial/where/tutorial005_py310.py new file mode 100644 index 0000000000..5251506970 --- /dev/null +++ b/docs_src/tutorial/where/tutorial005_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.age < 35) + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/where/tutorial006_py310.py b/docs_src/tutorial/where/tutorial006_py310.py new file mode 100644 index 0000000000..a3ab8507e3 --- /dev/null +++ b/docs_src/tutorial/where/tutorial006_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.age <= 35) + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/where/tutorial007_py310.py b/docs_src/tutorial/where/tutorial007_py310.py new file mode 100644 index 0000000000..589bc98671 --- /dev/null +++ b/docs_src/tutorial/where/tutorial007_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.age >= 35).where(Hero.age < 40) + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/where/tutorial008_py310.py b/docs_src/tutorial/where/tutorial008_py310.py new file mode 100644 index 0000000000..f32260c9f9 --- /dev/null +++ b/docs_src/tutorial/where/tutorial008_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.age >= 35, Hero.age < 40) + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/where/tutorial009_py310.py b/docs_src/tutorial/where/tutorial009_py310.py new file mode 100644 index 0000000000..0681d1c0a5 --- /dev/null +++ b/docs_src/tutorial/where/tutorial009_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, or_, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(or_(Hero.age <= 35, Hero.age > 90)) + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/where/tutorial010_py310.py b/docs_src/tutorial/where/tutorial010_py310.py new file mode 100644 index 0000000000..a65c47acf2 --- /dev/null +++ b/docs_src/tutorial/where/tutorial010_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where((Hero.age <= 35) | (Hero.age > 90)) + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/where/tutorial011_py310.py b/docs_src/tutorial/where/tutorial011_py310.py new file mode 100644 index 0000000000..73aa4aa8c0 --- /dev/null +++ b/docs_src/tutorial/where/tutorial011_py310.py @@ -0,0 +1,57 @@ +from sqlmodel import Field, Session, SQLModel, col, create_engine, select + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + secret_name: str + age: int | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + 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) + hero_4 = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) + hero_5 = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_6 = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) + hero_7 = Hero(name="Captain North America", secret_name="Esteban Rogelios", age=93) + + with Session(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + session.add(hero_4) + session.add(hero_5) + session.add(hero_6) + session.add(hero_7) + + session.commit() + + +def select_heroes(): + with Session(engine) as session: + statement = select(Hero).where(col(Hero.age) >= 35) + results = session.exec(statement) + for hero in results: + print(hero) + + +def main(): + create_db_and_tables() + create_heroes() + select_heroes() + + +if __name__ == "__main__": + main() diff --git a/tests/conftest.py b/tests/conftest.py index 2b8e5fc29e..7b2cfcd6d5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,6 @@ import shutil import subprocess +import sys from pathlib import Path from typing import Any, Callable, Dict, List, Union @@ -67,3 +68,9 @@ def new_print(*args): calls.append(data) return new_print + + +needs_py39 = pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python3.9+") +needs_py310 = pytest.mark.skipif( + sys.version_info < (3, 10), reason="requires python3.10+" +) diff --git a/tests/test_advanced/test_decimal/test_tutorial001_py310.py b/tests/test_advanced/test_decimal/test_tutorial001_py310.py new file mode 100644 index 0000000000..f58ea11a7c --- /dev/null +++ b/tests/test_advanced/test_decimal/test_tutorial001_py310.py @@ -0,0 +1,45 @@ +from decimal import Decimal +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + +expected_calls = [ + [ + "Hero 1:", + { + "name": "Deadpond", + "age": None, + "id": 1, + "secret_name": "Dive Wilson", + "money": Decimal("1.100"), + }, + ], + [ + "Hero 2:", + { + "name": "Rusty-Man", + "age": 48, + "id": 3, + "secret_name": "Tommy Sharp", + "money": Decimal("2.200"), + }, + ], + ["Total money: 3.300"], +] + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.advanced.decimal import tutorial001_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_automatic_id_none_refresh/test_tutorial001_py310_tutorial002_py310.py b/tests/test_tutorial/test_automatic_id_none_refresh/test_tutorial001_py310_tutorial002_py310.py new file mode 100644 index 0000000000..9ffcd8ae33 --- /dev/null +++ b/tests/test_tutorial/test_automatic_id_none_refresh/test_tutorial001_py310_tutorial002_py310.py @@ -0,0 +1,163 @@ +from typing import Any, Dict, List, Union +from unittest.mock import patch + +from sqlmodel import create_engine + +from tests.conftest import get_testing_print_function, needs_py310 + + +def check_calls(calls: List[List[Union[str, Dict[str, Any]]]]): + assert calls[0] == ["Before interacting with the database"] + assert calls[1] == [ + "Hero 1:", + { + "id": None, + "name": "Deadpond", + "secret_name": "Dive Wilson", + "age": None, + }, + ] + assert calls[2] == [ + "Hero 2:", + { + "id": None, + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "age": None, + }, + ] + assert calls[3] == [ + "Hero 3:", + { + "id": None, + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "age": 48, + }, + ] + assert calls[4] == ["After adding to the session"] + assert calls[5] == [ + "Hero 1:", + { + "id": None, + "name": "Deadpond", + "secret_name": "Dive Wilson", + "age": None, + }, + ] + assert calls[6] == [ + "Hero 2:", + { + "id": None, + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "age": None, + }, + ] + assert calls[7] == [ + "Hero 3:", + { + "id": None, + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "age": 48, + }, + ] + assert calls[8] == ["After committing the session"] + assert calls[9] == ["Hero 1:", {}] + assert calls[10] == ["Hero 2:", {}] + assert calls[11] == ["Hero 3:", {}] + assert calls[12] == ["After committing the session, show IDs"] + assert calls[13] == ["Hero 1 ID:", 1] + assert calls[14] == ["Hero 2 ID:", 2] + assert calls[15] == ["Hero 3 ID:", 3] + assert calls[16] == ["After committing the session, show names"] + assert calls[17] == ["Hero 1 name:", "Deadpond"] + assert calls[18] == ["Hero 2 name:", "Spider-Boy"] + assert calls[19] == ["Hero 3 name:", "Rusty-Man"] + assert calls[20] == ["After refreshing the heroes"] + assert calls[21] == [ + "Hero 1:", + { + "id": 1, + "name": "Deadpond", + "secret_name": "Dive Wilson", + "age": None, + }, + ] + assert calls[22] == [ + "Hero 2:", + { + "id": 2, + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "age": None, + }, + ] + assert calls[23] == [ + "Hero 3:", + { + "id": 3, + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "age": 48, + }, + ] + assert calls[24] == ["After the session closes"] + assert calls[21] == [ + "Hero 1:", + { + "id": 1, + "name": "Deadpond", + "secret_name": "Dive Wilson", + "age": None, + }, + ] + assert calls[22] == [ + "Hero 2:", + { + "id": 2, + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "age": None, + }, + ] + assert calls[23] == [ + "Hero 3:", + { + "id": 3, + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "age": 48, + }, + ] + + +@needs_py310 +def test_tutorial_001(clear_sqlmodel): + from docs_src.tutorial.automatic_id_none_refresh import tutorial001_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + check_calls(calls) + + +@needs_py310 +def test_tutorial_002(clear_sqlmodel): + from docs_src.tutorial.automatic_id_none_refresh import tutorial002_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + check_calls(calls) diff --git a/tests/test_tutorial/test_code_structure/test_tutorial001_py310.py b/tests/test_tutorial/test_code_structure/test_tutorial001_py310.py new file mode 100644 index 0000000000..31965600e6 --- /dev/null +++ b/tests/test_tutorial/test_code_structure/test_tutorial001_py310.py @@ -0,0 +1,38 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + +expected_calls = [ + [ + "Created hero:", + { + "id": 1, + "name": "Deadpond", + "age": None, + "secret_name": "Dive Wilson", + "team_id": 1, + }, + ], + [ + "Hero's team:", + {"name": "Z-Force", "headquarters": "Sister Margaret’s Bar", "id": 1}, + ], +] + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.code_structure.tutorial001_py310 import app, database + + database.sqlite_url = "sqlite://" + database.engine = create_engine(database.sqlite_url) + app.engine = database.engine + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + app.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_code_structure/test_tutorial001_py39.py b/tests/test_tutorial/test_code_structure/test_tutorial001_py39.py new file mode 100644 index 0000000000..b101dea055 --- /dev/null +++ b/tests/test_tutorial/test_code_structure/test_tutorial001_py39.py @@ -0,0 +1,38 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py39 + +expected_calls = [ + [ + "Created hero:", + { + "id": 1, + "name": "Deadpond", + "age": None, + "secret_name": "Dive Wilson", + "team_id": 1, + }, + ], + [ + "Hero's team:", + {"name": "Z-Force", "headquarters": "Sister Margaret’s Bar", "id": 1}, + ], +] + + +@needs_py39 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.code_structure.tutorial001_py39 import app, database + + database.sqlite_url = "sqlite://" + database.engine = create_engine(database.sqlite_url) + app.engine = database.engine + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + app.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_code_structure/test_tutorial002_py310.py b/tests/test_tutorial/test_code_structure/test_tutorial002_py310.py new file mode 100644 index 0000000000..5744aa9e35 --- /dev/null +++ b/tests/test_tutorial/test_code_structure/test_tutorial002_py310.py @@ -0,0 +1,38 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + +expected_calls = [ + [ + "Created hero:", + { + "id": 1, + "name": "Deadpond", + "age": None, + "secret_name": "Dive Wilson", + "team_id": 1, + }, + ], + [ + "Hero's team:", + {"name": "Z-Force", "headquarters": "Sister Margaret’s Bar", "id": 1}, + ], +] + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.code_structure.tutorial002_py310 import app, database + + database.sqlite_url = "sqlite://" + database.engine = create_engine(database.sqlite_url) + app.engine = database.engine + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + app.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_code_structure/test_tutorial002_py39.py b/tests/test_tutorial/test_code_structure/test_tutorial002_py39.py new file mode 100644 index 0000000000..bae15c37e7 --- /dev/null +++ b/tests/test_tutorial/test_code_structure/test_tutorial002_py39.py @@ -0,0 +1,38 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py39 + +expected_calls = [ + [ + "Created hero:", + { + "id": 1, + "name": "Deadpond", + "age": None, + "secret_name": "Dive Wilson", + "team_id": 1, + }, + ], + [ + "Hero's team:", + {"name": "Z-Force", "headquarters": "Sister Margaret’s Bar", "id": 1}, + ], +] + + +@needs_py39 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.code_structure.tutorial002_py39 import app, database + + database.sqlite_url = "sqlite://" + database.engine = create_engine(database.sqlite_url) + app.engine = database.engine + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + app.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_create_connected_tables/test_tutorial001_py310.py b/tests/test_tutorial/test_connect/test_create_connected_tables/test_tutorial001_py310.py new file mode 100644 index 0000000000..ec2990ebfb --- /dev/null +++ b/tests/test_tutorial/test_connect/test_create_connected_tables/test_tutorial001_py310.py @@ -0,0 +1,17 @@ +from sqlalchemy import inspect +from sqlalchemy.engine.reflection import Inspector +from sqlmodel import create_engine + +from ....conftest import needs_py310 + + +@needs_py310 +def test_tutorial001(clear_sqlmodel): + from docs_src.tutorial.connect.create_tables import tutorial001_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + mod.main() + insp: Inspector = inspect(mod.engine) + assert insp.has_table(str(mod.Hero.__tablename__)) + assert insp.has_table(str(mod.Team.__tablename__)) diff --git a/tests/test_tutorial/test_connect/test_delete/test_tutorial001_py310.py b/tests/test_tutorial/test_connect/test_delete/test_tutorial001_py310.py new file mode 100644 index 0000000000..edc70b8a3d --- /dev/null +++ b/tests/test_tutorial/test_connect/test_delete/test_tutorial001_py310.py @@ -0,0 +1,73 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ....conftest import get_testing_print_function, needs_py310 + +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", + }, + ], +] + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.connect.delete import tutorial001_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_insert/test_tutorial001_py310.py b/tests/test_tutorial/test_connect/test_insert/test_tutorial001_py310.py new file mode 100644 index 0000000000..854c0068ab --- /dev/null +++ b/tests/test_tutorial/test_connect/test_insert/test_tutorial001_py310.py @@ -0,0 +1,53 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ....conftest import get_testing_print_function, needs_py310 + +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", + }, + ], +] + + +@needs_py310 +def test_tutorial001(clear_sqlmodel): + from docs_src.tutorial.connect.insert import tutorial001_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial001_py310_tutorial002_py310.py b/tests/test_tutorial/test_connect/test_select/test_tutorial001_py310_tutorial002_py310.py new file mode 100644 index 0000000000..01762f3e7e --- /dev/null +++ b/tests/test_tutorial/test_connect/test_select/test_tutorial001_py310_tutorial002_py310.py @@ -0,0 +1,92 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ....conftest import get_testing_print_function, needs_py310 + +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"}, + ], +] + + +@needs_py310 +def test_tutorial001(clear_sqlmodel): + from docs_src.tutorial.connect.select import tutorial001_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls + + +@needs_py310 +def test_tutorial002(clear_sqlmodel): + from docs_src.tutorial.connect.select import tutorial002_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial003_py310.py b/tests/test_tutorial/test_connect/test_select/test_tutorial003_py310.py new file mode 100644 index 0000000000..69abccb0aa --- /dev/null +++ b/tests/test_tutorial/test_connect/test_select/test_tutorial003_py310.py @@ -0,0 +1,89 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ....conftest import get_testing_print_function, needs_py310 + +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, + ], +] + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.connect.select import tutorial003_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial004_py310.py b/tests/test_tutorial/test_connect/test_select/test_tutorial004_py310.py new file mode 100644 index 0000000000..72974ec6cf --- /dev/null +++ b/tests/test_tutorial/test_connect/test_select/test_tutorial004_py310.py @@ -0,0 +1,63 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ....conftest import get_testing_print_function, needs_py310 + +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", + }, + ], +] + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.connect.select import tutorial004_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial005_py310.py b/tests/test_tutorial/test_connect/test_select/test_tutorial005_py310.py new file mode 100644 index 0000000000..a7332c18a7 --- /dev/null +++ b/tests/test_tutorial/test_connect/test_select/test_tutorial005_py310.py @@ -0,0 +1,65 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ....conftest import get_testing_print_function, needs_py310 + +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"}, + ], +] + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.connect.select import tutorial005_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_update/test_tutorial001_py310.py b/tests/test_tutorial/test_connect/test_update/test_tutorial001_py310.py new file mode 100644 index 0000000000..f3702654c2 --- /dev/null +++ b/tests/test_tutorial/test_connect/test_update/test_tutorial001_py310.py @@ -0,0 +1,63 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ....conftest import get_testing_print_function, needs_py310 + +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", + }, + ], +] + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.connect.update import tutorial001_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_create_db_and_table/test_tutorial001_py310.py b/tests/test_tutorial/test_create_db_and_table/test_tutorial001_py310.py new file mode 100644 index 0000000000..465b9f9d58 --- /dev/null +++ b/tests/test_tutorial/test_create_db_and_table/test_tutorial001_py310.py @@ -0,0 +1,19 @@ +from pathlib import Path + +from ...conftest import coverage_run, needs_py310 + + +@needs_py310 +def test_create_db_and_table(cov_tmp_path: Path): + module = "docs_src.tutorial.create_db_and_table.tutorial001_py310" + result = coverage_run(module=module, cwd=cov_tmp_path) + assert "BEGIN" in result.stdout + assert 'PRAGMA main.table_info("hero")' in result.stdout + assert "CREATE TABLE hero (" in result.stdout + assert "id INTEGER NOT NULL," in result.stdout + assert "name VARCHAR NOT NULL," in result.stdout + assert "secret_name VARCHAR NOT NULL," in result.stdout + assert "age INTEGER," in result.stdout + assert "PRIMARY KEY (id)" in result.stdout + assert ")" in result.stdout + assert "COMMIT" in result.stdout diff --git a/tests/test_tutorial/test_create_db_and_table/test_tutorial002_py310.py b/tests/test_tutorial/test_create_db_and_table/test_tutorial002_py310.py new file mode 100644 index 0000000000..3ca3186b9e --- /dev/null +++ b/tests/test_tutorial/test_create_db_and_table/test_tutorial002_py310.py @@ -0,0 +1,16 @@ +from sqlalchemy import inspect +from sqlalchemy.engine.reflection import Inspector +from sqlmodel import create_engine + +from ...conftest import needs_py310 + + +@needs_py310 +def test_create_db_and_table(clear_sqlmodel): + from docs_src.tutorial.create_db_and_table import tutorial002_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + mod.create_db_and_tables() + insp: Inspector = inspect(mod.engine) + assert insp.has_table(str(mod.Hero.__tablename__)) diff --git a/tests/test_tutorial/test_create_db_and_table/test_tutorial003_py310.py b/tests/test_tutorial/test_create_db_and_table/test_tutorial003_py310.py new file mode 100644 index 0000000000..a1806ce250 --- /dev/null +++ b/tests/test_tutorial/test_create_db_and_table/test_tutorial003_py310.py @@ -0,0 +1,16 @@ +from sqlalchemy import inspect +from sqlalchemy.engine.reflection import Inspector +from sqlmodel import create_engine + +from ...conftest import needs_py310 + + +@needs_py310 +def test_create_db_and_table(clear_sqlmodel): + from docs_src.tutorial.create_db_and_table import tutorial003_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + mod.create_db_and_tables() + insp: Inspector = inspect(mod.engine) + assert insp.has_table(str(mod.Hero.__tablename__)) diff --git a/tests/test_tutorial/test_delete/test_tutorial001_py310_tutorial002_py310.py b/tests/test_tutorial/test_delete/test_tutorial001_py310_tutorial002_py310.py new file mode 100644 index 0000000000..0f97e7489f --- /dev/null +++ b/tests/test_tutorial/test_delete/test_tutorial001_py310_tutorial002_py310.py @@ -0,0 +1,88 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + +expected_calls = [ + [ + "Hero 1:", + {"id": 2, "name": "Spider-Boy", "secret_name": "Pedro Parqueador", "age": None}, + ], + [ + "Hero 2:", + { + "id": 7, + "name": "Captain North America", + "secret_name": "Esteban Rogelios", + "age": 93, + }, + ], + [ + "Updated hero 1:", + { + "id": 2, + "name": "Spider-Youngster", + "secret_name": "Pedro Parqueador", + "age": 16, + }, + ], + [ + "Updated hero 2:", + { + "id": 7, + "name": "Captain North America Except Canada", + "secret_name": "Esteban Rogelios", + "age": 110, + }, + ], + [ + "Hero: ", + { + "id": 2, + "name": "Spider-Youngster", + "secret_name": "Pedro Parqueador", + "age": 16, + }, + ], + [ + "Deleted hero:", + { + "id": 2, + "name": "Spider-Youngster", + "secret_name": "Pedro Parqueador", + "age": 16, + }, + ], + ["There's no hero named Spider-Youngster"], +] + + +@needs_py310 +def test_tutorial001(clear_sqlmodel): + from docs_src.tutorial.delete import tutorial001_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls + + +@needs_py310 +def test_tutorial002(clear_sqlmodel): + from docs_src.tutorial.delete import tutorial002_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_py310_tests_main.py b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_py310_tests_main.py new file mode 100644 index 0000000000..781de7c772 --- /dev/null +++ b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_py310_tests_main.py @@ -0,0 +1,25 @@ +import subprocess +from pathlib import Path + +from ....conftest import needs_py310 + + +@needs_py310 +def test_run_tests(clear_sqlmodel): + from docs_src.tutorial.fastapi.app_testing.tutorial001_py310 import test_main as mod + + test_path = Path(mod.__file__).resolve().parent + top_level_path = Path(__file__).resolve().parent.parent.parent.parent.parent + result = subprocess.run( + [ + "coverage", + "run", + "--parallel-mode", + "-m", + "pytest", + test_path, + ], + cwd=top_level_path, + capture_output=True, + ) + assert result.returncode == 0, result.stdout.decode("utf-8") diff --git a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_py39_tests_main.py b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_py39_tests_main.py new file mode 100644 index 0000000000..6dbcc80d56 --- /dev/null +++ b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_py39_tests_main.py @@ -0,0 +1,25 @@ +import subprocess +from pathlib import Path + +from ....conftest import needs_py39 + + +@needs_py39 +def test_run_tests(clear_sqlmodel): + from docs_src.tutorial.fastapi.app_testing.tutorial001_py39 import test_main as mod + + test_path = Path(mod.__file__).resolve().parent + top_level_path = Path(__file__).resolve().parent.parent.parent.parent.parent + result = subprocess.run( + [ + "coverage", + "run", + "--parallel-mode", + "-m", + "pytest", + test_path, + ], + cwd=top_level_path, + capture_output=True, + ) + assert result.returncode == 0, result.stdout.decode("utf-8") diff --git a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests_main.py b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests_main.py index 8051c7ac64..d7c1329b38 100644 --- a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests_main.py +++ b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests_main.py @@ -1,83 +1,22 @@ -import importlib - -import pytest -from fastapi.testclient import TestClient -from sqlalchemy import inspect -from sqlalchemy.engine.reflection import Inspector -from sqlmodel import Session, create_engine - -from docs_src.tutorial.fastapi.app_testing.tutorial001 import main as app_mod -from docs_src.tutorial.fastapi.app_testing.tutorial001 import test_main as test_mod -from docs_src.tutorial.fastapi.app_testing.tutorial001.test_main import ( - client_fixture, - session_fixture, -) - -assert session_fixture, "This keeps the session fixture used below" -assert client_fixture, "This keeps the client fixture used below" - - -@pytest.fixture(name="prepare", autouse=True) -def prepare_fixture(clear_sqlmodel): - # Trigger side effects of registering table models in SQLModel - # This has to be called after clear_sqlmodel, but before the session_fixture - # That's why the extra custom fixture here - importlib.reload(app_mod) - importlib.reload(test_mod) - - -def test_create_hero(session: Session, client: TestClient): - test_mod.test_create_hero(client) - - -def test_create_hero_incomplete(session: Session, client: TestClient): - test_mod.test_create_hero_incomplete(client) - - -def test_create_hero_invalid(session: Session, client: TestClient): - test_mod.test_create_hero_invalid(client) - - -def test_read_heroes(session: Session, client: TestClient): - test_mod.test_read_heroes(session=session, client=client) - - -def test_read_hero(session: Session, client: TestClient): - test_mod.test_read_hero(session=session, client=client) - - -def test_update_hero(session: Session, client: TestClient): - test_mod.test_update_hero(session=session, client=client) - - -def test_delete_hero(session: Session, client: TestClient): - test_mod.test_delete_hero(session=session, client=client) - - -def test_startup(): - app_mod.engine = create_engine("sqlite://") - app_mod.on_startup() - insp: Inspector = inspect(app_mod.engine) - assert insp.has_table(str(app_mod.Hero.__tablename__)) - - -def test_get_session(): - app_mod.engine = create_engine("sqlite://") - for session in app_mod.get_session(): - assert isinstance(session, Session) - assert session.bind == app_mod.engine - - -def test_read_hero_not_found(client: TestClient): - response = client.get("/heroes/9000") - assert response.status_code == 404 - - -def test_update_hero_not_found(client: TestClient): - response = client.patch("/heroes/9000", json={"name": "Very-Rusty-Man"}) - assert response.status_code == 404 - - -def test_delete_hero_not_found(client: TestClient): - response = client.delete("/heroes/9000") - assert response.status_code == 404 +import subprocess +from pathlib import Path + + +def test_run_tests(clear_sqlmodel): + from docs_src.tutorial.fastapi.app_testing.tutorial001 import test_main as mod + + test_path = Path(mod.__file__).resolve().parent + top_level_path = Path(__file__).resolve().parent.parent.parent.parent.parent + result = subprocess.run( + [ + "coverage", + "run", + "--parallel-mode", + "-m", + "pytest", + test_path, + ], + cwd=top_level_path, + capture_output=True, + ) + assert result.returncode == 0, result.stdout.decode("utf-8") diff --git a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py310.py new file mode 100644 index 0000000000..133b287630 --- /dev/null +++ b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py310.py @@ -0,0 +1,331 @@ +from fastapi.testclient import TestClient +from sqlmodel import create_engine +from sqlmodel.pool import StaticPool + +from ....conftest import needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.fastapi.delete import tutorial001_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine( + mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool + ) + + with TestClient(mod.app) as client: + hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} + hero2_data = { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "id": 9000, + } + hero3_data = { + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "age": 48, + } + response = client.post("/heroes/", json=hero1_data) + assert response.status_code == 200, response.text + response = client.post("/heroes/", json=hero2_data) + assert response.status_code == 200, response.text + hero2 = response.json() + hero2_id = hero2["id"] + response = client.post("/heroes/", json=hero3_data) + assert response.status_code == 200, response.text + response = client.get(f"/heroes/{hero2_id}") + assert response.status_code == 200, response.text + response = client.get("/heroes/9000") + assert response.status_code == 404, response.text + response = client.get("/heroes/") + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 3 + response = client.patch( + f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} + ) + assert response.status_code == 200, response.text + response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) + assert response.status_code == 404, response.text + + response = client.delete(f"/heroes/{hero2_id}") + assert response.status_code == 200, response.text + response = client.get("/heroes/") + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 2 + + response = client.delete("/heroes/9000") + assert response.status_code == 404, response.text + + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Offset", + "type": "integer", + "default": 0, + }, + "name": "offset", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "maximum": 100.0, + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Heroes Heroes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/HeroRead" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/heroes/{hero_id}": { + "get": { + "summary": "Read Hero", + "operationId": "read_hero_heroes__hero_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "delete": { + "summary": "Delete Hero", + "operationId": "delete_hero_heroes__hero_id__delete", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "patch": { + "summary": "Update Hero", + "operationId": "update_hero_heroes__hero_id__patch", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroUpdate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "HeroCreate": { + "title": "HeroCreate", + "required": ["name", "secret_name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "HeroRead": { + "title": "HeroRead", + "required": ["name", "secret_name", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "id": {"title": "Id", "type": "integer"}, + }, + }, + "HeroUpdate": { + "title": "HeroUpdate", + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py39.py new file mode 100644 index 0000000000..5aac8cb11f --- /dev/null +++ b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py39.py @@ -0,0 +1,331 @@ +from fastapi.testclient import TestClient +from sqlmodel import create_engine +from sqlmodel.pool import StaticPool + +from ....conftest import needs_py39 + + +@needs_py39 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.fastapi.delete import tutorial001_py39 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine( + mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool + ) + + with TestClient(mod.app) as client: + hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} + hero2_data = { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "id": 9000, + } + hero3_data = { + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "age": 48, + } + response = client.post("/heroes/", json=hero1_data) + assert response.status_code == 200, response.text + response = client.post("/heroes/", json=hero2_data) + assert response.status_code == 200, response.text + hero2 = response.json() + hero2_id = hero2["id"] + response = client.post("/heroes/", json=hero3_data) + assert response.status_code == 200, response.text + response = client.get(f"/heroes/{hero2_id}") + assert response.status_code == 200, response.text + response = client.get("/heroes/9000") + assert response.status_code == 404, response.text + response = client.get("/heroes/") + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 3 + response = client.patch( + f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} + ) + assert response.status_code == 200, response.text + response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) + assert response.status_code == 404, response.text + + response = client.delete(f"/heroes/{hero2_id}") + assert response.status_code == 200, response.text + response = client.get("/heroes/") + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 2 + + response = client.delete("/heroes/9000") + assert response.status_code == 404, response.text + + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Offset", + "type": "integer", + "default": 0, + }, + "name": "offset", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "maximum": 100.0, + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Heroes Heroes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/HeroRead" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/heroes/{hero_id}": { + "get": { + "summary": "Read Hero", + "operationId": "read_hero_heroes__hero_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "delete": { + "summary": "Delete Hero", + "operationId": "delete_hero_heroes__hero_id__delete", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "patch": { + "summary": "Update Hero", + "operationId": "update_hero_heroes__hero_id__patch", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroUpdate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "HeroCreate": { + "title": "HeroCreate", + "required": ["name", "secret_name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "HeroRead": { + "title": "HeroRead", + "required": ["name", "secret_name", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "id": {"title": "Id", "type": "integer"}, + }, + }, + "HeroUpdate": { + "title": "HeroUpdate", + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py310.py new file mode 100644 index 0000000000..ee0d89ac55 --- /dev/null +++ b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py310.py @@ -0,0 +1,255 @@ +from fastapi.testclient import TestClient +from sqlmodel import create_engine +from sqlmodel.pool import StaticPool + +from ....conftest import needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.fastapi.limit_and_offset import tutorial001_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine( + mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool + ) + + with TestClient(mod.app) as client: + hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} + hero2_data = { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "id": 9000, + } + hero3_data = { + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "age": 48, + } + response = client.post("/heroes/", json=hero1_data) + assert response.status_code == 200, response.text + response = client.post("/heroes/", json=hero2_data) + assert response.status_code == 200, response.text + hero2 = response.json() + hero_id = hero2["id"] + response = client.post("/heroes/", json=hero3_data) + assert response.status_code == 200, response.text + response = client.get(f"/heroes/{hero_id}") + assert response.status_code == 200, response.text + response = client.get("/heroes/9000") + assert response.status_code == 404, response.text + + response = client.get("/heroes/") + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 3 + + response = client.get("/heroes/", params={"limit": 2}) + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 2 + assert data[0]["name"] == hero1_data["name"] + assert data[1]["name"] == hero2_data["name"] + + response = client.get("/heroes/", params={"offset": 1}) + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 2 + assert data[0]["name"] == hero2_data["name"] + assert data[1]["name"] == hero3_data["name"] + + response = client.get("/heroes/", params={"offset": 1, "limit": 1}) + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 1 + assert data[0]["name"] == hero2_data["name"] + + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Offset", + "type": "integer", + "default": 0, + }, + "name": "offset", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "maximum": 100.0, + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Heroes Heroes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/HeroRead" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/heroes/{hero_id}": { + "get": { + "summary": "Read Hero", + "operationId": "read_hero_heroes__hero_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "HeroCreate": { + "title": "HeroCreate", + "required": ["name", "secret_name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "HeroRead": { + "title": "HeroRead", + "required": ["name", "secret_name", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "id": {"title": "Id", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py39.py new file mode 100644 index 0000000000..f4ef44abc5 --- /dev/null +++ b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py39.py @@ -0,0 +1,255 @@ +from fastapi.testclient import TestClient +from sqlmodel import create_engine +from sqlmodel.pool import StaticPool + +from ....conftest import needs_py39 + + +@needs_py39 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.fastapi.limit_and_offset import tutorial001_py39 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine( + mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool + ) + + with TestClient(mod.app) as client: + hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} + hero2_data = { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "id": 9000, + } + hero3_data = { + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "age": 48, + } + response = client.post("/heroes/", json=hero1_data) + assert response.status_code == 200, response.text + response = client.post("/heroes/", json=hero2_data) + assert response.status_code == 200, response.text + hero2 = response.json() + hero_id = hero2["id"] + response = client.post("/heroes/", json=hero3_data) + assert response.status_code == 200, response.text + response = client.get(f"/heroes/{hero_id}") + assert response.status_code == 200, response.text + response = client.get("/heroes/9000") + assert response.status_code == 404, response.text + + response = client.get("/heroes/") + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 3 + + response = client.get("/heroes/", params={"limit": 2}) + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 2 + assert data[0]["name"] == hero1_data["name"] + assert data[1]["name"] == hero2_data["name"] + + response = client.get("/heroes/", params={"offset": 1}) + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 2 + assert data[0]["name"] == hero2_data["name"] + assert data[1]["name"] == hero3_data["name"] + + response = client.get("/heroes/", params={"offset": 1, "limit": 1}) + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 1 + assert data[0]["name"] == hero2_data["name"] + + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Offset", + "type": "integer", + "default": 0, + }, + "name": "offset", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "maximum": 100.0, + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Heroes Heroes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/HeroRead" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/heroes/{hero_id}": { + "get": { + "summary": "Read Hero", + "operationId": "read_hero_heroes__hero_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "HeroCreate": { + "title": "HeroCreate", + "required": ["name", "secret_name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "HeroRead": { + "title": "HeroRead", + "required": ["name", "secret_name", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "id": {"title": "Id", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py310.py new file mode 100644 index 0000000000..080a907e0e --- /dev/null +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py310.py @@ -0,0 +1,203 @@ +from fastapi.testclient import TestClient +from sqlalchemy import inspect +from sqlalchemy.engine.reflection import Inspector +from sqlmodel import create_engine +from sqlmodel.pool import StaticPool + +from ....conftest import needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.fastapi.multiple_models import tutorial001_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine( + mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool + ) + + with TestClient(mod.app) as client: + hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} + hero2_data = { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "id": 9000, + } + response = client.post("/heroes/", json=hero1_data) + data = response.json() + + assert response.status_code == 200, response.text + assert data["name"] == hero1_data["name"] + assert data["secret_name"] == hero1_data["secret_name"] + assert data["id"] is not None + assert data["age"] is None + + response = client.post("/heroes/", json=hero2_data) + data = response.json() + + assert response.status_code == 200, response.text + assert data["name"] == hero2_data["name"] + assert data["secret_name"] == hero2_data["secret_name"] + assert data["id"] != hero2_data["id"], ( + "Now it's not possible to predefine the ID from the request, " + "it's now set by the database" + ) + assert data["age"] is None + + response = client.get("/heroes/") + data = response.json() + + assert response.status_code == 200, response.text + assert len(data) == 2 + assert data[0]["name"] == hero1_data["name"] + assert data[0]["secret_name"] == hero1_data["secret_name"] + assert data[1]["name"] == hero2_data["name"] + assert data[1]["secret_name"] == hero2_data["secret_name"] + assert data[1]["id"] != hero2_data["id"] + + response = client.get("/openapi.json") + data = response.json() + + assert response.status_code == 200, response.text + + assert data == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Heroes Heroes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/HeroRead" + }, + } + } + }, + } + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "HeroCreate": { + "title": "HeroCreate", + "required": ["name", "secret_name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "HeroRead": { + "title": "HeroRead", + "required": ["id", "name", "secret_name"], + "type": "object", + "properties": { + "id": {"title": "Id", "type": "integer"}, + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } + + # Test inherited indexes + insp: Inspector = inspect(mod.engine) + indexes = insp.get_indexes(str(mod.Hero.__tablename__)) + expected_indexes = [ + { + "name": "ix_hero_name", + "dialect_options": {}, + "column_names": ["name"], + "unique": 0, + }, + { + "name": "ix_hero_age", + "dialect_options": {}, + "column_names": ["age"], + "unique": 0, + }, + ] + for index in expected_indexes: + assert index in indexes, "This expected index should be in the indexes in DB" + # Now that this index was checked, remove it from the list of indexes + indexes.pop(indexes.index(index)) + assert len(indexes) == 0, "The database should only have the expected indexes" diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py39.py new file mode 100644 index 0000000000..7c320093ae --- /dev/null +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py39.py @@ -0,0 +1,203 @@ +from fastapi.testclient import TestClient +from sqlalchemy import inspect +from sqlalchemy.engine.reflection import Inspector +from sqlmodel import create_engine +from sqlmodel.pool import StaticPool + +from ....conftest import needs_py39 + + +@needs_py39 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.fastapi.multiple_models import tutorial001_py39 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine( + mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool + ) + + with TestClient(mod.app) as client: + hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} + hero2_data = { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "id": 9000, + } + response = client.post("/heroes/", json=hero1_data) + data = response.json() + + assert response.status_code == 200, response.text + assert data["name"] == hero1_data["name"] + assert data["secret_name"] == hero1_data["secret_name"] + assert data["id"] is not None + assert data["age"] is None + + response = client.post("/heroes/", json=hero2_data) + data = response.json() + + assert response.status_code == 200, response.text + assert data["name"] == hero2_data["name"] + assert data["secret_name"] == hero2_data["secret_name"] + assert data["id"] != hero2_data["id"], ( + "Now it's not possible to predefine the ID from the request, " + "it's now set by the database" + ) + assert data["age"] is None + + response = client.get("/heroes/") + data = response.json() + + assert response.status_code == 200, response.text + assert len(data) == 2 + assert data[0]["name"] == hero1_data["name"] + assert data[0]["secret_name"] == hero1_data["secret_name"] + assert data[1]["name"] == hero2_data["name"] + assert data[1]["secret_name"] == hero2_data["secret_name"] + assert data[1]["id"] != hero2_data["id"] + + response = client.get("/openapi.json") + data = response.json() + + assert response.status_code == 200, response.text + + assert data == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Heroes Heroes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/HeroRead" + }, + } + } + }, + } + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "HeroCreate": { + "title": "HeroCreate", + "required": ["name", "secret_name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "HeroRead": { + "title": "HeroRead", + "required": ["id", "name", "secret_name"], + "type": "object", + "properties": { + "id": {"title": "Id", "type": "integer"}, + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } + + # Test inherited indexes + insp: Inspector = inspect(mod.engine) + indexes = insp.get_indexes(str(mod.Hero.__tablename__)) + expected_indexes = [ + { + "name": "ix_hero_name", + "dialect_options": {}, + "column_names": ["name"], + "unique": 0, + }, + { + "name": "ix_hero_age", + "dialect_options": {}, + "column_names": ["age"], + "unique": 0, + }, + ] + for index in expected_indexes: + assert index in indexes, "This expected index should be in the indexes in DB" + # Now that this index was checked, remove it from the list of indexes + indexes.pop(indexes.index(index)) + assert len(indexes) == 0, "The database should only have the expected indexes" diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py310.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py310.py new file mode 100644 index 0000000000..20195c6fdf --- /dev/null +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py310.py @@ -0,0 +1,203 @@ +from fastapi.testclient import TestClient +from sqlalchemy import inspect +from sqlalchemy.engine.reflection import Inspector +from sqlmodel import create_engine +from sqlmodel.pool import StaticPool + +from ....conftest import needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.fastapi.multiple_models import tutorial002_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine( + mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool + ) + + with TestClient(mod.app) as client: + hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} + hero2_data = { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "id": 9000, + } + response = client.post("/heroes/", json=hero1_data) + data = response.json() + + assert response.status_code == 200, response.text + assert data["name"] == hero1_data["name"] + assert data["secret_name"] == hero1_data["secret_name"] + assert data["id"] is not None + assert data["age"] is None + + response = client.post("/heroes/", json=hero2_data) + data = response.json() + + assert response.status_code == 200, response.text + assert data["name"] == hero2_data["name"] + assert data["secret_name"] == hero2_data["secret_name"] + assert data["id"] != hero2_data["id"], ( + "Now it's not possible to predefine the ID from the request, " + "it's now set by the database" + ) + assert data["age"] is None + + response = client.get("/heroes/") + data = response.json() + + assert response.status_code == 200, response.text + assert len(data) == 2 + assert data[0]["name"] == hero1_data["name"] + assert data[0]["secret_name"] == hero1_data["secret_name"] + assert data[1]["name"] == hero2_data["name"] + assert data[1]["secret_name"] == hero2_data["secret_name"] + assert data[1]["id"] != hero2_data["id"] + + response = client.get("/openapi.json") + data = response.json() + + assert response.status_code == 200, response.text + + assert data == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Heroes Heroes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/HeroRead" + }, + } + } + }, + } + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "HeroCreate": { + "title": "HeroCreate", + "required": ["name", "secret_name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "HeroRead": { + "title": "HeroRead", + "required": ["name", "secret_name", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "id": {"title": "Id", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } + + # Test inherited indexes + insp: Inspector = inspect(mod.engine) + indexes = insp.get_indexes(str(mod.Hero.__tablename__)) + expected_indexes = [ + { + "name": "ix_hero_age", + "dialect_options": {}, + "column_names": ["age"], + "unique": 0, + }, + { + "name": "ix_hero_name", + "dialect_options": {}, + "column_names": ["name"], + "unique": 0, + }, + ] + for index in expected_indexes: + assert index in indexes, "This expected index should be in the indexes in DB" + # Now that this index was checked, remove it from the list of indexes + indexes.pop(indexes.index(index)) + assert len(indexes) == 0, "The database should only have the expected indexes" diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py39.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py39.py new file mode 100644 index 0000000000..45b061b401 --- /dev/null +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py39.py @@ -0,0 +1,203 @@ +from fastapi.testclient import TestClient +from sqlalchemy import inspect +from sqlalchemy.engine.reflection import Inspector +from sqlmodel import create_engine +from sqlmodel.pool import StaticPool + +from ....conftest import needs_py39 + + +@needs_py39 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.fastapi.multiple_models import tutorial002_py39 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine( + mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool + ) + + with TestClient(mod.app) as client: + hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} + hero2_data = { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "id": 9000, + } + response = client.post("/heroes/", json=hero1_data) + data = response.json() + + assert response.status_code == 200, response.text + assert data["name"] == hero1_data["name"] + assert data["secret_name"] == hero1_data["secret_name"] + assert data["id"] is not None + assert data["age"] is None + + response = client.post("/heroes/", json=hero2_data) + data = response.json() + + assert response.status_code == 200, response.text + assert data["name"] == hero2_data["name"] + assert data["secret_name"] == hero2_data["secret_name"] + assert data["id"] != hero2_data["id"], ( + "Now it's not possible to predefine the ID from the request, " + "it's now set by the database" + ) + assert data["age"] is None + + response = client.get("/heroes/") + data = response.json() + + assert response.status_code == 200, response.text + assert len(data) == 2 + assert data[0]["name"] == hero1_data["name"] + assert data[0]["secret_name"] == hero1_data["secret_name"] + assert data[1]["name"] == hero2_data["name"] + assert data[1]["secret_name"] == hero2_data["secret_name"] + assert data[1]["id"] != hero2_data["id"] + + response = client.get("/openapi.json") + data = response.json() + + assert response.status_code == 200, response.text + + assert data == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Heroes Heroes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/HeroRead" + }, + } + } + }, + } + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "HeroCreate": { + "title": "HeroCreate", + "required": ["name", "secret_name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "HeroRead": { + "title": "HeroRead", + "required": ["name", "secret_name", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "id": {"title": "Id", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } + + # Test inherited indexes + insp: Inspector = inspect(mod.engine) + indexes = insp.get_indexes(str(mod.Hero.__tablename__)) + expected_indexes = [ + { + "name": "ix_hero_age", + "dialect_options": {}, + "column_names": ["age"], + "unique": 0, + }, + { + "name": "ix_hero_name", + "dialect_options": {}, + "column_names": ["name"], + "unique": 0, + }, + ] + for index in expected_indexes: + assert index in indexes, "This expected index should be in the indexes in DB" + # Now that this index was checked, remove it from the list of indexes + indexes.pop(indexes.index(index)) + assert len(indexes) == 0, "The database should only have the expected indexes" diff --git a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py310.py new file mode 100644 index 0000000000..2e0a97e780 --- /dev/null +++ b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py310.py @@ -0,0 +1,201 @@ +from fastapi.testclient import TestClient +from sqlmodel import create_engine +from sqlmodel.pool import StaticPool + +from ....conftest import needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.fastapi.read_one import tutorial001_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine( + mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool + ) + + with TestClient(mod.app) as client: + hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} + hero2_data = { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "id": 9000, + } + response = client.post("/heroes/", json=hero1_data) + assert response.status_code == 200, response.text + response = client.post("/heroes/", json=hero2_data) + assert response.status_code == 200, response.text + hero2 = response.json() + response = client.get("/heroes/") + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 2 + + hero_id = hero2["id"] + response = client.get(f"/heroes/{hero_id}") + assert response.status_code == 200, response.text + data = response.json() + assert data == hero2 + + response = client.get("/heroes/9000") + assert response.status_code == 404, response.text + + response = client.get("/openapi.json") + data = response.json() + + assert response.status_code == 200, response.text + + assert data == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Heroes Heroes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/HeroRead" + }, + } + } + }, + } + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/heroes/{hero_id}": { + "get": { + "summary": "Read Hero", + "operationId": "read_hero_heroes__hero_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "HeroCreate": { + "title": "HeroCreate", + "required": ["name", "secret_name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "HeroRead": { + "title": "HeroRead", + "required": ["name", "secret_name", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "id": {"title": "Id", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py39.py new file mode 100644 index 0000000000..a663eccac3 --- /dev/null +++ b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py39.py @@ -0,0 +1,201 @@ +from fastapi.testclient import TestClient +from sqlmodel import create_engine +from sqlmodel.pool import StaticPool + +from ....conftest import needs_py39 + + +@needs_py39 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.fastapi.read_one import tutorial001_py39 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine( + mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool + ) + + with TestClient(mod.app) as client: + hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} + hero2_data = { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "id": 9000, + } + response = client.post("/heroes/", json=hero1_data) + assert response.status_code == 200, response.text + response = client.post("/heroes/", json=hero2_data) + assert response.status_code == 200, response.text + hero2 = response.json() + response = client.get("/heroes/") + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 2 + + hero_id = hero2["id"] + response = client.get(f"/heroes/{hero_id}") + assert response.status_code == 200, response.text + data = response.json() + assert data == hero2 + + response = client.get("/heroes/9000") + assert response.status_code == 404, response.text + + response = client.get("/openapi.json") + data = response.json() + + assert response.status_code == 200, response.text + + assert data == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Heroes Heroes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/HeroRead" + }, + } + } + }, + } + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/heroes/{hero_id}": { + "get": { + "summary": "Read Hero", + "operationId": "read_hero_heroes__hero_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "HeroCreate": { + "title": "HeroCreate", + "required": ["name", "secret_name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "HeroRead": { + "title": "HeroRead", + "required": ["name", "secret_name", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "id": {"title": "Id", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py310.py new file mode 100644 index 0000000000..dae7db3378 --- /dev/null +++ b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py310.py @@ -0,0 +1,638 @@ +from fastapi.testclient import TestClient +from sqlmodel import create_engine +from sqlmodel.pool import StaticPool + +from ....conftest import needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.fastapi.relationships import tutorial001_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine( + mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool + ) + + with TestClient(mod.app) as client: + team_preventers = {"name": "Preventers", "headquarters": "Sharp Tower"} + team_z_force = {"name": "Z-Force", "headquarters": "Sister Margaret’s Bar"} + response = client.post("/teams/", json=team_preventers) + assert response.status_code == 200, response.text + team_preventers_data = response.json() + team_preventers_id = team_preventers_data["id"] + response = client.post("/teams/", json=team_z_force) + assert response.status_code == 200, response.text + team_z_force_data = response.json() + team_z_force_id = team_z_force_data["id"] + response = client.get("/teams/") + data = response.json() + assert len(data) == 2 + response = client.get("/teams/9000") + assert response.status_code == 404, response.text + response = client.patch( + f"/teams/{team_preventers_id}", json={"headquarters": "Preventers Tower"} + ) + data = response.json() + assert response.status_code == 200, response.text + assert data["name"] == team_preventers["name"] + assert data["headquarters"] == "Preventers Tower" + response = client.patch("/teams/9000", json={"name": "Freedom League"}) + assert response.status_code == 404, response.text + + hero1_data = { + "name": "Deadpond", + "secret_name": "Dive Wilson", + "team_id": team_z_force_id, + } + hero2_data = { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "id": 9000, + } + hero3_data = { + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "age": 48, + "team_id": team_preventers_id, + } + response = client.post("/heroes/", json=hero1_data) + assert response.status_code == 200, response.text + hero1 = response.json() + hero1_id = hero1["id"] + response = client.post("/heroes/", json=hero2_data) + assert response.status_code == 200, response.text + hero2 = response.json() + hero2_id = hero2["id"] + response = client.post("/heroes/", json=hero3_data) + assert response.status_code == 200, response.text + response = client.get("/heroes/9000") + assert response.status_code == 404, response.text + response = client.get("/heroes/") + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 3 + response = client.get(f"/heroes/{hero1_id}") + assert response.status_code == 200, response.text + data = response.json() + assert data["name"] == hero1_data["name"] + assert data["team"]["name"] == team_z_force["name"] + response = client.patch( + f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} + ) + assert response.status_code == 200, response.text + response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) + assert response.status_code == 404, response.text + response = client.delete(f"/heroes/{hero2_id}") + assert response.status_code == 200, response.text + response = client.get("/heroes/") + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 2 + response = client.delete("/heroes/9000") + assert response.status_code == 404, response.text + + response = client.get(f"/teams/{team_preventers_id}") + data = response.json() + assert response.status_code == 200, response.text + assert data["name"] == team_preventers_data["name"] + assert data["heroes"][0]["name"] == hero3_data["name"] + + response = client.delete(f"/teams/{team_preventers_id}") + assert response.status_code == 200, response.text + response = client.delete("/teams/9000") + assert response.status_code == 404, response.text + response = client.get("/teams/") + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 1 + + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Offset", + "type": "integer", + "default": 0, + }, + "name": "offset", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "maximum": 100.0, + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Heroes Heroes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/HeroRead" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/heroes/{hero_id}": { + "get": { + "summary": "Read Hero", + "operationId": "read_hero_heroes__hero_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroReadWithTeam" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "delete": { + "summary": "Delete Hero", + "operationId": "delete_hero_heroes__hero_id__delete", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "patch": { + "summary": "Update Hero", + "operationId": "update_hero_heroes__hero_id__patch", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroUpdate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/teams/": { + "get": { + "summary": "Read Teams", + "operationId": "read_teams_teams__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Offset", + "type": "integer", + "default": 0, + }, + "name": "offset", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "maximum": 100.0, + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Teams Teams Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/TeamRead" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "post": { + "summary": "Create Team", + "operationId": "create_team_teams__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/teams/{team_id}": { + "get": { + "summary": "Read Team", + "operationId": "read_team_teams__team_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Team Id", "type": "integer"}, + "name": "team_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamReadWithHeroes" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "delete": { + "summary": "Delete Team", + "operationId": "delete_team_teams__team_id__delete", + "parameters": [ + { + "required": True, + "schema": {"title": "Team Id", "type": "integer"}, + "name": "team_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "patch": { + "summary": "Update Team", + "operationId": "update_team_teams__team_id__patch", + "parameters": [ + { + "required": True, + "schema": {"title": "Team Id", "type": "integer"}, + "name": "team_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamUpdate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "HeroCreate": { + "title": "HeroCreate", + "required": ["name", "secret_name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "team_id": {"title": "Team Id", "type": "integer"}, + }, + }, + "HeroRead": { + "title": "HeroRead", + "required": ["name", "secret_name", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "team_id": {"title": "Team Id", "type": "integer"}, + "id": {"title": "Id", "type": "integer"}, + }, + }, + "HeroReadWithTeam": { + "title": "HeroReadWithTeam", + "required": ["name", "secret_name", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "team_id": {"title": "Team Id", "type": "integer"}, + "id": {"title": "Id", "type": "integer"}, + "team": {"$ref": "#/components/schemas/TeamRead"}, + }, + }, + "HeroUpdate": { + "title": "HeroUpdate", + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "team_id": {"title": "Team Id", "type": "integer"}, + }, + }, + "TeamCreate": { + "title": "TeamCreate", + "required": ["name", "headquarters"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "headquarters": {"title": "Headquarters", "type": "string"}, + }, + }, + "TeamRead": { + "title": "TeamRead", + "required": ["name", "headquarters", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "headquarters": {"title": "Headquarters", "type": "string"}, + "id": {"title": "Id", "type": "integer"}, + }, + }, + "TeamReadWithHeroes": { + "title": "TeamReadWithHeroes", + "required": ["name", "headquarters", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "headquarters": {"title": "Headquarters", "type": "string"}, + "id": {"title": "Id", "type": "integer"}, + "heroes": { + "title": "Heroes", + "type": "array", + "items": {"$ref": "#/components/schemas/HeroRead"}, + "default": [], + }, + }, + }, + "TeamUpdate": { + "title": "TeamUpdate", + "type": "object", + "properties": { + "id": {"title": "Id", "type": "integer"}, + "name": {"title": "Name", "type": "string"}, + "headquarters": {"title": "Headquarters", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py39.py new file mode 100644 index 0000000000..72dee33434 --- /dev/null +++ b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py39.py @@ -0,0 +1,638 @@ +from fastapi.testclient import TestClient +from sqlmodel import create_engine +from sqlmodel.pool import StaticPool + +from ....conftest import needs_py39 + + +@needs_py39 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.fastapi.relationships import tutorial001_py39 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine( + mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool + ) + + with TestClient(mod.app) as client: + team_preventers = {"name": "Preventers", "headquarters": "Sharp Tower"} + team_z_force = {"name": "Z-Force", "headquarters": "Sister Margaret’s Bar"} + response = client.post("/teams/", json=team_preventers) + assert response.status_code == 200, response.text + team_preventers_data = response.json() + team_preventers_id = team_preventers_data["id"] + response = client.post("/teams/", json=team_z_force) + assert response.status_code == 200, response.text + team_z_force_data = response.json() + team_z_force_id = team_z_force_data["id"] + response = client.get("/teams/") + data = response.json() + assert len(data) == 2 + response = client.get("/teams/9000") + assert response.status_code == 404, response.text + response = client.patch( + f"/teams/{team_preventers_id}", json={"headquarters": "Preventers Tower"} + ) + data = response.json() + assert response.status_code == 200, response.text + assert data["name"] == team_preventers["name"] + assert data["headquarters"] == "Preventers Tower" + response = client.patch("/teams/9000", json={"name": "Freedom League"}) + assert response.status_code == 404, response.text + + hero1_data = { + "name": "Deadpond", + "secret_name": "Dive Wilson", + "team_id": team_z_force_id, + } + hero2_data = { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "id": 9000, + } + hero3_data = { + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "age": 48, + "team_id": team_preventers_id, + } + response = client.post("/heroes/", json=hero1_data) + assert response.status_code == 200, response.text + hero1 = response.json() + hero1_id = hero1["id"] + response = client.post("/heroes/", json=hero2_data) + assert response.status_code == 200, response.text + hero2 = response.json() + hero2_id = hero2["id"] + response = client.post("/heroes/", json=hero3_data) + assert response.status_code == 200, response.text + response = client.get("/heroes/9000") + assert response.status_code == 404, response.text + response = client.get("/heroes/") + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 3 + response = client.get(f"/heroes/{hero1_id}") + assert response.status_code == 200, response.text + data = response.json() + assert data["name"] == hero1_data["name"] + assert data["team"]["name"] == team_z_force["name"] + response = client.patch( + f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} + ) + assert response.status_code == 200, response.text + response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) + assert response.status_code == 404, response.text + response = client.delete(f"/heroes/{hero2_id}") + assert response.status_code == 200, response.text + response = client.get("/heroes/") + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 2 + response = client.delete("/heroes/9000") + assert response.status_code == 404, response.text + + response = client.get(f"/teams/{team_preventers_id}") + data = response.json() + assert response.status_code == 200, response.text + assert data["name"] == team_preventers_data["name"] + assert data["heroes"][0]["name"] == hero3_data["name"] + + response = client.delete(f"/teams/{team_preventers_id}") + assert response.status_code == 200, response.text + response = client.delete("/teams/9000") + assert response.status_code == 404, response.text + response = client.get("/teams/") + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 1 + + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Offset", + "type": "integer", + "default": 0, + }, + "name": "offset", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "maximum": 100.0, + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Heroes Heroes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/HeroRead" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/heroes/{hero_id}": { + "get": { + "summary": "Read Hero", + "operationId": "read_hero_heroes__hero_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroReadWithTeam" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "delete": { + "summary": "Delete Hero", + "operationId": "delete_hero_heroes__hero_id__delete", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "patch": { + "summary": "Update Hero", + "operationId": "update_hero_heroes__hero_id__patch", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroUpdate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/teams/": { + "get": { + "summary": "Read Teams", + "operationId": "read_teams_teams__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Offset", + "type": "integer", + "default": 0, + }, + "name": "offset", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "maximum": 100.0, + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Teams Teams Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/TeamRead" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "post": { + "summary": "Create Team", + "operationId": "create_team_teams__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/teams/{team_id}": { + "get": { + "summary": "Read Team", + "operationId": "read_team_teams__team_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Team Id", "type": "integer"}, + "name": "team_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamReadWithHeroes" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "delete": { + "summary": "Delete Team", + "operationId": "delete_team_teams__team_id__delete", + "parameters": [ + { + "required": True, + "schema": {"title": "Team Id", "type": "integer"}, + "name": "team_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "patch": { + "summary": "Update Team", + "operationId": "update_team_teams__team_id__patch", + "parameters": [ + { + "required": True, + "schema": {"title": "Team Id", "type": "integer"}, + "name": "team_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamUpdate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "HeroCreate": { + "title": "HeroCreate", + "required": ["name", "secret_name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "team_id": {"title": "Team Id", "type": "integer"}, + }, + }, + "HeroRead": { + "title": "HeroRead", + "required": ["name", "secret_name", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "team_id": {"title": "Team Id", "type": "integer"}, + "id": {"title": "Id", "type": "integer"}, + }, + }, + "HeroReadWithTeam": { + "title": "HeroReadWithTeam", + "required": ["name", "secret_name", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "team_id": {"title": "Team Id", "type": "integer"}, + "id": {"title": "Id", "type": "integer"}, + "team": {"$ref": "#/components/schemas/TeamRead"}, + }, + }, + "HeroUpdate": { + "title": "HeroUpdate", + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "team_id": {"title": "Team Id", "type": "integer"}, + }, + }, + "TeamCreate": { + "title": "TeamCreate", + "required": ["name", "headquarters"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "headquarters": {"title": "Headquarters", "type": "string"}, + }, + }, + "TeamRead": { + "title": "TeamRead", + "required": ["name", "headquarters", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "headquarters": {"title": "Headquarters", "type": "string"}, + "id": {"title": "Id", "type": "integer"}, + }, + }, + "TeamReadWithHeroes": { + "title": "TeamReadWithHeroes", + "required": ["name", "headquarters", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "headquarters": {"title": "Headquarters", "type": "string"}, + "id": {"title": "Id", "type": "integer"}, + "heroes": { + "title": "Heroes", + "type": "array", + "items": {"$ref": "#/components/schemas/HeroRead"}, + "default": [], + }, + }, + }, + "TeamUpdate": { + "title": "TeamUpdate", + "type": "object", + "properties": { + "id": {"title": "Id", "type": "integer"}, + "name": {"title": "Name", "type": "string"}, + "headquarters": {"title": "Headquarters", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py310.py new file mode 100644 index 0000000000..4acb0068a1 --- /dev/null +++ b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py310.py @@ -0,0 +1,144 @@ +from fastapi.testclient import TestClient +from sqlmodel import create_engine +from sqlmodel.pool import StaticPool + +from ....conftest import needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.fastapi.response_model import tutorial001_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine( + mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool + ) + + with TestClient(mod.app) as client: + hero_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} + response = client.post("/heroes/", json=hero_data) + data = response.json() + + assert response.status_code == 200, response.text + assert data["name"] == hero_data["name"] + assert data["secret_name"] == hero_data["secret_name"] + assert data["id"] is not None + assert data["age"] is None + + response = client.get("/heroes/") + data = response.json() + + assert response.status_code == 200, response.text + assert len(data) == 1 + assert data[0]["name"] == hero_data["name"] + assert data[0]["secret_name"] == hero_data["secret_name"] + + response = client.get("/openapi.json") + data = response.json() + + assert response.status_code == 200, response.text + + assert data == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Heroes Heroes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/Hero" + }, + } + } + }, + } + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Hero"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Hero"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "Hero": { + "title": "Hero", + "required": ["name", "secret_name"], + "type": "object", + "properties": { + "id": {"title": "Id", "type": "integer"}, + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py39.py new file mode 100644 index 0000000000..20f3f52313 --- /dev/null +++ b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py39.py @@ -0,0 +1,144 @@ +from fastapi.testclient import TestClient +from sqlmodel import create_engine +from sqlmodel.pool import StaticPool + +from ....conftest import needs_py39 + + +@needs_py39 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.fastapi.response_model import tutorial001_py39 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine( + mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool + ) + + with TestClient(mod.app) as client: + hero_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} + response = client.post("/heroes/", json=hero_data) + data = response.json() + + assert response.status_code == 200, response.text + assert data["name"] == hero_data["name"] + assert data["secret_name"] == hero_data["secret_name"] + assert data["id"] is not None + assert data["age"] is None + + response = client.get("/heroes/") + data = response.json() + + assert response.status_code == 200, response.text + assert len(data) == 1 + assert data[0]["name"] == hero_data["name"] + assert data[0]["secret_name"] == hero_data["secret_name"] + + response = client.get("/openapi.json") + data = response.json() + + assert response.status_code == 200, response.text + + assert data == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Heroes Heroes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/Hero" + }, + } + } + }, + } + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Hero"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Hero"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "Hero": { + "title": "Hero", + "required": ["name", "secret_name"], + "type": "object", + "properties": { + "id": {"title": "Id", "type": "integer"}, + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py310.py new file mode 100644 index 0000000000..f0c5416bdf --- /dev/null +++ b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py310.py @@ -0,0 +1,333 @@ +from fastapi.testclient import TestClient +from sqlmodel import create_engine +from sqlmodel.pool import StaticPool + +from ....conftest import needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.fastapi.session_with_dependency import ( + tutorial001_py310 as mod, + ) + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine( + mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool + ) + + with TestClient(mod.app) as client: + hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} + hero2_data = { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "id": 9000, + } + hero3_data = { + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "age": 48, + } + response = client.post("/heroes/", json=hero1_data) + assert response.status_code == 200, response.text + response = client.post("/heroes/", json=hero2_data) + assert response.status_code == 200, response.text + hero2 = response.json() + hero2_id = hero2["id"] + response = client.post("/heroes/", json=hero3_data) + assert response.status_code == 200, response.text + response = client.get(f"/heroes/{hero2_id}") + assert response.status_code == 200, response.text + response = client.get("/heroes/9000") + assert response.status_code == 404, response.text + response = client.get("/heroes/") + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 3 + response = client.patch( + f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} + ) + assert response.status_code == 200, response.text + response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) + assert response.status_code == 404, response.text + + response = client.delete(f"/heroes/{hero2_id}") + assert response.status_code == 200, response.text + response = client.get("/heroes/") + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 2 + + response = client.delete("/heroes/9000") + assert response.status_code == 404, response.text + + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Offset", + "type": "integer", + "default": 0, + }, + "name": "offset", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "maximum": 100.0, + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Heroes Heroes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/HeroRead" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/heroes/{hero_id}": { + "get": { + "summary": "Read Hero", + "operationId": "read_hero_heroes__hero_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "delete": { + "summary": "Delete Hero", + "operationId": "delete_hero_heroes__hero_id__delete", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "patch": { + "summary": "Update Hero", + "operationId": "update_hero_heroes__hero_id__patch", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroUpdate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "HeroCreate": { + "title": "HeroCreate", + "required": ["name", "secret_name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "HeroRead": { + "title": "HeroRead", + "required": ["name", "secret_name", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "id": {"title": "Id", "type": "integer"}, + }, + }, + "HeroUpdate": { + "title": "HeroUpdate", + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py39.py new file mode 100644 index 0000000000..5b911c8462 --- /dev/null +++ b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py39.py @@ -0,0 +1,333 @@ +from fastapi.testclient import TestClient +from sqlmodel import create_engine +from sqlmodel.pool import StaticPool + +from ....conftest import needs_py39 + + +@needs_py39 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.fastapi.session_with_dependency import ( + tutorial001_py39 as mod, + ) + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine( + mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool + ) + + with TestClient(mod.app) as client: + hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} + hero2_data = { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "id": 9000, + } + hero3_data = { + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "age": 48, + } + response = client.post("/heroes/", json=hero1_data) + assert response.status_code == 200, response.text + response = client.post("/heroes/", json=hero2_data) + assert response.status_code == 200, response.text + hero2 = response.json() + hero2_id = hero2["id"] + response = client.post("/heroes/", json=hero3_data) + assert response.status_code == 200, response.text + response = client.get(f"/heroes/{hero2_id}") + assert response.status_code == 200, response.text + response = client.get("/heroes/9000") + assert response.status_code == 404, response.text + response = client.get("/heroes/") + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 3 + response = client.patch( + f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} + ) + assert response.status_code == 200, response.text + response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) + assert response.status_code == 404, response.text + + response = client.delete(f"/heroes/{hero2_id}") + assert response.status_code == 200, response.text + response = client.get("/heroes/") + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 2 + + response = client.delete("/heroes/9000") + assert response.status_code == 404, response.text + + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Offset", + "type": "integer", + "default": 0, + }, + "name": "offset", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "maximum": 100.0, + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Heroes Heroes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/HeroRead" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/heroes/{hero_id}": { + "get": { + "summary": "Read Hero", + "operationId": "read_hero_heroes__hero_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "delete": { + "summary": "Delete Hero", + "operationId": "delete_hero_heroes__hero_id__delete", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "patch": { + "summary": "Update Hero", + "operationId": "update_hero_heroes__hero_id__patch", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroUpdate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "HeroCreate": { + "title": "HeroCreate", + "required": ["name", "secret_name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "HeroRead": { + "title": "HeroRead", + "required": ["name", "secret_name", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "id": {"title": "Id", "type": "integer"}, + }, + }, + "HeroUpdate": { + "title": "HeroUpdate", + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001_py310.py new file mode 100644 index 0000000000..d85d9ee5b2 --- /dev/null +++ b/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001_py310.py @@ -0,0 +1,150 @@ +from fastapi.testclient import TestClient +from sqlmodel import create_engine +from sqlmodel.pool import StaticPool + +from ....conftest import needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.fastapi.simple_hero_api import tutorial001_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine( + mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool + ) + + with TestClient(mod.app) as client: + hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} + hero2_data = { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "id": 9000, + } + response = client.post("/heroes/", json=hero1_data) + data = response.json() + + assert response.status_code == 200, response.text + assert data["name"] == hero1_data["name"] + assert data["secret_name"] == hero1_data["secret_name"] + assert data["id"] is not None + assert data["age"] is None + + response = client.post("/heroes/", json=hero2_data) + data = response.json() + + assert response.status_code == 200, response.text + assert data["name"] == hero2_data["name"] + assert data["secret_name"] == hero2_data["secret_name"] + assert data["id"] == hero2_data["id"], ( + "Up to this point it's still possible to " + "set the ID of the hero in the request" + ) + assert data["age"] is None + + response = client.get("/heroes/") + data = response.json() + + assert response.status_code == 200, response.text + assert len(data) == 2 + assert data[0]["name"] == hero1_data["name"] + assert data[0]["secret_name"] == hero1_data["secret_name"] + assert data[1]["name"] == hero2_data["name"] + assert data[1]["secret_name"] == hero2_data["secret_name"] + assert data[1]["id"] == hero2_data["id"] + + response = client.get("/openapi.json") + data = response.json() + + assert response.status_code == 200, response.text + + assert data == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Hero"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "Hero": { + "title": "Hero", + "required": ["name", "secret_name"], + "type": "object", + "properties": { + "id": {"title": "Id", "type": "integer"}, + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py310.py new file mode 100644 index 0000000000..6cec87a0a7 --- /dev/null +++ b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py310.py @@ -0,0 +1,595 @@ +from fastapi.testclient import TestClient +from sqlmodel import create_engine +from sqlmodel.pool import StaticPool + +from ....conftest import needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.fastapi.teams import tutorial001_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine( + mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool + ) + + with TestClient(mod.app) as client: + hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} + hero2_data = { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "id": 9000, + } + hero3_data = { + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "age": 48, + } + response = client.post("/heroes/", json=hero1_data) + assert response.status_code == 200, response.text + response = client.post("/heroes/", json=hero2_data) + assert response.status_code == 200, response.text + hero2 = response.json() + hero2_id = hero2["id"] + response = client.post("/heroes/", json=hero3_data) + assert response.status_code == 200, response.text + response = client.get(f"/heroes/{hero2_id}") + assert response.status_code == 200, response.text + response = client.get("/heroes/9000") + assert response.status_code == 404, response.text + response = client.get("/heroes/") + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 3 + response = client.patch( + f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} + ) + assert response.status_code == 200, response.text + response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) + assert response.status_code == 404, response.text + response = client.delete(f"/heroes/{hero2_id}") + assert response.status_code == 200, response.text + response = client.get("/heroes/") + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 2 + response = client.delete("/heroes/9000") + assert response.status_code == 404, response.text + + team_preventers = {"name": "Preventers", "headquarters": "Sharp Tower"} + team_z_force = {"name": "Z-Force", "headquarters": "Sister Margaret’s Bar"} + response = client.post("/teams/", json=team_preventers) + assert response.status_code == 200, response.text + team_preventers_data = response.json() + team_preventers_id = team_preventers_data["id"] + response = client.post("/teams/", json=team_z_force) + assert response.status_code == 200, response.text + team_z_force_data = response.json() + team_z_force_data["id"] + response = client.get("/teams/") + data = response.json() + assert len(data) == 2 + response = client.get(f"/teams/{team_preventers_id}") + data = response.json() + assert response.status_code == 200, response.text + assert data == team_preventers_data + response = client.get("/teams/9000") + assert response.status_code == 404, response.text + response = client.patch( + f"/teams/{team_preventers_id}", json={"headquarters": "Preventers Tower"} + ) + data = response.json() + assert response.status_code == 200, response.text + assert data["name"] == team_preventers["name"] + assert data["headquarters"] == "Preventers Tower" + response = client.patch("/teams/9000", json={"name": "Freedom League"}) + assert response.status_code == 404, response.text + response = client.delete(f"/teams/{team_preventers_id}") + assert response.status_code == 200, response.text + response = client.delete("/teams/9000") + assert response.status_code == 404, response.text + response = client.get("/teams/") + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 1 + + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Offset", + "type": "integer", + "default": 0, + }, + "name": "offset", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "maximum": 100.0, + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Heroes Heroes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/HeroRead" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/heroes/{hero_id}": { + "get": { + "summary": "Read Hero", + "operationId": "read_hero_heroes__hero_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "delete": { + "summary": "Delete Hero", + "operationId": "delete_hero_heroes__hero_id__delete", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "patch": { + "summary": "Update Hero", + "operationId": "update_hero_heroes__hero_id__patch", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroUpdate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/teams/": { + "get": { + "summary": "Read Teams", + "operationId": "read_teams_teams__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Offset", + "type": "integer", + "default": 0, + }, + "name": "offset", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "maximum": 100.0, + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Teams Teams Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/TeamRead" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "post": { + "summary": "Create Team", + "operationId": "create_team_teams__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/teams/{team_id}": { + "get": { + "summary": "Read Team", + "operationId": "read_team_teams__team_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Team Id", "type": "integer"}, + "name": "team_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "delete": { + "summary": "Delete Team", + "operationId": "delete_team_teams__team_id__delete", + "parameters": [ + { + "required": True, + "schema": {"title": "Team Id", "type": "integer"}, + "name": "team_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "patch": { + "summary": "Update Team", + "operationId": "update_team_teams__team_id__patch", + "parameters": [ + { + "required": True, + "schema": {"title": "Team Id", "type": "integer"}, + "name": "team_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamUpdate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "HeroCreate": { + "title": "HeroCreate", + "required": ["name", "secret_name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "team_id": {"title": "Team Id", "type": "integer"}, + }, + }, + "HeroRead": { + "title": "HeroRead", + "required": ["name", "secret_name", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "team_id": {"title": "Team Id", "type": "integer"}, + "id": {"title": "Id", "type": "integer"}, + }, + }, + "HeroUpdate": { + "title": "HeroUpdate", + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "team_id": {"title": "Team Id", "type": "integer"}, + }, + }, + "TeamCreate": { + "title": "TeamCreate", + "required": ["name", "headquarters"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "headquarters": {"title": "Headquarters", "type": "string"}, + }, + }, + "TeamRead": { + "title": "TeamRead", + "required": ["name", "headquarters", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "headquarters": {"title": "Headquarters", "type": "string"}, + "id": {"title": "Id", "type": "integer"}, + }, + }, + "TeamUpdate": { + "title": "TeamUpdate", + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "headquarters": {"title": "Headquarters", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py39.py new file mode 100644 index 0000000000..70279f5b8d --- /dev/null +++ b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py39.py @@ -0,0 +1,595 @@ +from fastapi.testclient import TestClient +from sqlmodel import create_engine +from sqlmodel.pool import StaticPool + +from ....conftest import needs_py39 + + +@needs_py39 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.fastapi.teams import tutorial001_py39 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine( + mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool + ) + + with TestClient(mod.app) as client: + hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} + hero2_data = { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "id": 9000, + } + hero3_data = { + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "age": 48, + } + response = client.post("/heroes/", json=hero1_data) + assert response.status_code == 200, response.text + response = client.post("/heroes/", json=hero2_data) + assert response.status_code == 200, response.text + hero2 = response.json() + hero2_id = hero2["id"] + response = client.post("/heroes/", json=hero3_data) + assert response.status_code == 200, response.text + response = client.get(f"/heroes/{hero2_id}") + assert response.status_code == 200, response.text + response = client.get("/heroes/9000") + assert response.status_code == 404, response.text + response = client.get("/heroes/") + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 3 + response = client.patch( + f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} + ) + assert response.status_code == 200, response.text + response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) + assert response.status_code == 404, response.text + response = client.delete(f"/heroes/{hero2_id}") + assert response.status_code == 200, response.text + response = client.get("/heroes/") + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 2 + response = client.delete("/heroes/9000") + assert response.status_code == 404, response.text + + team_preventers = {"name": "Preventers", "headquarters": "Sharp Tower"} + team_z_force = {"name": "Z-Force", "headquarters": "Sister Margaret’s Bar"} + response = client.post("/teams/", json=team_preventers) + assert response.status_code == 200, response.text + team_preventers_data = response.json() + team_preventers_id = team_preventers_data["id"] + response = client.post("/teams/", json=team_z_force) + assert response.status_code == 200, response.text + team_z_force_data = response.json() + team_z_force_data["id"] + response = client.get("/teams/") + data = response.json() + assert len(data) == 2 + response = client.get(f"/teams/{team_preventers_id}") + data = response.json() + assert response.status_code == 200, response.text + assert data == team_preventers_data + response = client.get("/teams/9000") + assert response.status_code == 404, response.text + response = client.patch( + f"/teams/{team_preventers_id}", json={"headquarters": "Preventers Tower"} + ) + data = response.json() + assert response.status_code == 200, response.text + assert data["name"] == team_preventers["name"] + assert data["headquarters"] == "Preventers Tower" + response = client.patch("/teams/9000", json={"name": "Freedom League"}) + assert response.status_code == 404, response.text + response = client.delete(f"/teams/{team_preventers_id}") + assert response.status_code == 200, response.text + response = client.delete("/teams/9000") + assert response.status_code == 404, response.text + response = client.get("/teams/") + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 1 + + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Offset", + "type": "integer", + "default": 0, + }, + "name": "offset", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "maximum": 100.0, + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Heroes Heroes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/HeroRead" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/heroes/{hero_id}": { + "get": { + "summary": "Read Hero", + "operationId": "read_hero_heroes__hero_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "delete": { + "summary": "Delete Hero", + "operationId": "delete_hero_heroes__hero_id__delete", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "patch": { + "summary": "Update Hero", + "operationId": "update_hero_heroes__hero_id__patch", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroUpdate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/teams/": { + "get": { + "summary": "Read Teams", + "operationId": "read_teams_teams__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Offset", + "type": "integer", + "default": 0, + }, + "name": "offset", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "maximum": 100.0, + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Teams Teams Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/TeamRead" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "post": { + "summary": "Create Team", + "operationId": "create_team_teams__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/teams/{team_id}": { + "get": { + "summary": "Read Team", + "operationId": "read_team_teams__team_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Team Id", "type": "integer"}, + "name": "team_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "delete": { + "summary": "Delete Team", + "operationId": "delete_team_teams__team_id__delete", + "parameters": [ + { + "required": True, + "schema": {"title": "Team Id", "type": "integer"}, + "name": "team_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "patch": { + "summary": "Update Team", + "operationId": "update_team_teams__team_id__patch", + "parameters": [ + { + "required": True, + "schema": {"title": "Team Id", "type": "integer"}, + "name": "team_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamUpdate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "HeroCreate": { + "title": "HeroCreate", + "required": ["name", "secret_name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "team_id": {"title": "Team Id", "type": "integer"}, + }, + }, + "HeroRead": { + "title": "HeroRead", + "required": ["name", "secret_name", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "team_id": {"title": "Team Id", "type": "integer"}, + "id": {"title": "Id", "type": "integer"}, + }, + }, + "HeroUpdate": { + "title": "HeroUpdate", + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "team_id": {"title": "Team Id", "type": "integer"}, + }, + }, + "TeamCreate": { + "title": "TeamCreate", + "required": ["name", "headquarters"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "headquarters": {"title": "Headquarters", "type": "string"}, + }, + }, + "TeamRead": { + "title": "TeamRead", + "required": ["name", "headquarters", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "headquarters": {"title": "Headquarters", "type": "string"}, + "id": {"title": "Id", "type": "integer"}, + }, + }, + "TeamUpdate": { + "title": "TeamUpdate", + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "headquarters": {"title": "Headquarters", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py310.py new file mode 100644 index 0000000000..cf56e3cb01 --- /dev/null +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py310.py @@ -0,0 +1,310 @@ +from fastapi.testclient import TestClient +from sqlmodel import create_engine +from sqlmodel.pool import StaticPool + +from ....conftest import needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.fastapi.update import tutorial001_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine( + mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool + ) + + with TestClient(mod.app) as client: + hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} + hero2_data = { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "id": 9000, + } + hero3_data = { + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "age": 48, + } + response = client.post("/heroes/", json=hero1_data) + assert response.status_code == 200, response.text + response = client.post("/heroes/", json=hero2_data) + assert response.status_code == 200, response.text + hero2 = response.json() + hero2_id = hero2["id"] + response = client.post("/heroes/", json=hero3_data) + assert response.status_code == 200, response.text + hero3 = response.json() + hero3_id = hero3["id"] + response = client.get(f"/heroes/{hero2_id}") + assert response.status_code == 200, response.text + response = client.get("/heroes/9000") + assert response.status_code == 404, response.text + response = client.get("/heroes/") + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 3 + + response = client.patch( + f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} + ) + data = response.json() + assert response.status_code == 200, response.text + assert data["name"] == hero2_data["name"], "The name should not be set to none" + assert ( + data["secret_name"] == "Spider-Youngster" + ), "The secret name should be updated" + + response = client.patch(f"/heroes/{hero3_id}", json={"age": None}) + data = response.json() + assert response.status_code == 200, response.text + assert data["name"] == hero3_data["name"] + assert data["age"] is None, ( + "A field should be updatable to None, even if " "that's the default" + ) + + response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) + assert response.status_code == 404, response.text + + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Offset", + "type": "integer", + "default": 0, + }, + "name": "offset", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "maximum": 100.0, + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Heroes Heroes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/HeroRead" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/heroes/{hero_id}": { + "get": { + "summary": "Read Hero", + "operationId": "read_hero_heroes__hero_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "patch": { + "summary": "Update Hero", + "operationId": "update_hero_heroes__hero_id__patch", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroUpdate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "HeroCreate": { + "title": "HeroCreate", + "required": ["name", "secret_name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "HeroRead": { + "title": "HeroRead", + "required": ["name", "secret_name", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "id": {"title": "Id", "type": "integer"}, + }, + }, + "HeroUpdate": { + "title": "HeroUpdate", + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py39.py new file mode 100644 index 0000000000..b301ca3bf1 --- /dev/null +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py39.py @@ -0,0 +1,310 @@ +from fastapi.testclient import TestClient +from sqlmodel import create_engine +from sqlmodel.pool import StaticPool + +from ....conftest import needs_py39 + + +@needs_py39 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.fastapi.update import tutorial001_py39 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine( + mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool + ) + + with TestClient(mod.app) as client: + hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} + hero2_data = { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "id": 9000, + } + hero3_data = { + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "age": 48, + } + response = client.post("/heroes/", json=hero1_data) + assert response.status_code == 200, response.text + response = client.post("/heroes/", json=hero2_data) + assert response.status_code == 200, response.text + hero2 = response.json() + hero2_id = hero2["id"] + response = client.post("/heroes/", json=hero3_data) + assert response.status_code == 200, response.text + hero3 = response.json() + hero3_id = hero3["id"] + response = client.get(f"/heroes/{hero2_id}") + assert response.status_code == 200, response.text + response = client.get("/heroes/9000") + assert response.status_code == 404, response.text + response = client.get("/heroes/") + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 3 + + response = client.patch( + f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} + ) + data = response.json() + assert response.status_code == 200, response.text + assert data["name"] == hero2_data["name"], "The name should not be set to none" + assert ( + data["secret_name"] == "Spider-Youngster" + ), "The secret name should be updated" + + response = client.patch(f"/heroes/{hero3_id}", json={"age": None}) + data = response.json() + assert response.status_code == 200, response.text + assert data["name"] == hero3_data["name"] + assert data["age"] is None, ( + "A field should be updatable to None, even if " "that's the default" + ) + + response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) + assert response.status_code == 404, response.text + + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Offset", + "type": "integer", + "default": 0, + }, + "name": "offset", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "maximum": 100.0, + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Heroes Heroes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/HeroRead" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/heroes/{hero_id}": { + "get": { + "summary": "Read Hero", + "operationId": "read_hero_heroes__hero_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "patch": { + "summary": "Update Hero", + "operationId": "update_hero_heroes__hero_id__patch", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroUpdate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "HeroCreate": { + "title": "HeroCreate", + "required": ["name", "secret_name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "HeroRead": { + "title": "HeroRead", + "required": ["name", "secret_name", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + "id": {"title": "Id", "type": "integer"}, + }, + }, + "HeroUpdate": { + "title": "HeroUpdate", + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": {"title": "Age", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_indexes/test_tutorial001_py310.py b/tests/test_tutorial/test_indexes/test_tutorial001_py310.py new file mode 100644 index 0000000000..cfee262b2b --- /dev/null +++ b/tests/test_tutorial/test_indexes/test_tutorial001_py310.py @@ -0,0 +1,46 @@ +from unittest.mock import patch + +from sqlalchemy import inspect +from sqlalchemy.engine.reflection import Inspector +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.indexes import tutorial001_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == [ + [{"secret_name": "Dive Wilson", "age": None, "id": 1, "name": "Deadpond"}] + ] + + insp: Inspector = inspect(mod.engine) + indexes = insp.get_indexes(str(mod.Hero.__tablename__)) + expected_indexes = [ + { + "name": "ix_hero_name", + "dialect_options": {}, + "column_names": ["name"], + "unique": 0, + }, + { + "name": "ix_hero_age", + "dialect_options": {}, + "column_names": ["age"], + "unique": 0, + }, + ] + for index in expected_indexes: + assert index in indexes, "This expected index should be in the indexes in DB" + # Now that this index was checked, remove it from the list of indexes + indexes.pop(indexes.index(index)) + assert len(indexes) == 0, "The database should only have the expected indexes" diff --git a/tests/test_tutorial/test_indexes/test_tutorial006.py b/tests/test_tutorial/test_indexes/test_tutorial002.py similarity index 100% rename from tests/test_tutorial/test_indexes/test_tutorial006.py rename to tests/test_tutorial/test_indexes/test_tutorial002.py diff --git a/tests/test_tutorial/test_indexes/test_tutorial002_py310.py b/tests/test_tutorial/test_indexes/test_tutorial002_py310.py new file mode 100644 index 0000000000..089b6828e9 --- /dev/null +++ b/tests/test_tutorial/test_indexes/test_tutorial002_py310.py @@ -0,0 +1,47 @@ +from unittest.mock import patch + +from sqlalchemy import inspect +from sqlalchemy.engine.reflection import Inspector +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.indexes import tutorial002_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == [ + [{"name": "Tarantula", "secret_name": "Natalia Roman-on", "age": 32, "id": 4}], + [{"name": "Black Lion", "secret_name": "Trevor Challa", "age": 35, "id": 5}], + ] + + insp: Inspector = inspect(mod.engine) + indexes = insp.get_indexes(str(mod.Hero.__tablename__)) + expected_indexes = [ + { + "name": "ix_hero_name", + "dialect_options": {}, + "column_names": ["name"], + "unique": 0, + }, + { + "name": "ix_hero_age", + "dialect_options": {}, + "column_names": ["age"], + "unique": 0, + }, + ] + for index in expected_indexes: + assert index in indexes, "This expected index should be in the indexes in DB" + # Now that this index was checked, remove it from the list of indexes + indexes.pop(indexes.index(index)) + assert len(indexes) == 0, "The database should only have the expected indexes" diff --git a/tests/test_tutorial/test_insert/test_tutorial001_py310.py b/tests/test_tutorial/test_insert/test_tutorial001_py310.py new file mode 100644 index 0000000000..47cbc4cde6 --- /dev/null +++ b/tests/test_tutorial/test_insert/test_tutorial001_py310.py @@ -0,0 +1,30 @@ +from sqlmodel import Session, create_engine, select + +from ...conftest import needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.insert import tutorial001_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + mod.main() + with Session(mod.engine) as session: + heroes = session.exec(select(mod.Hero)).all() + heroes_by_name = {hero.name: hero for hero in heroes} + deadpond = heroes_by_name["Deadpond"] + spider_boy = heroes_by_name["Spider-Boy"] + rusty_man = heroes_by_name["Rusty-Man"] + assert deadpond.name == "Deadpond" + assert deadpond.age is None + assert deadpond.id is not None + assert deadpond.secret_name == "Dive Wilson" + assert spider_boy.name == "Spider-Boy" + assert spider_boy.age is None + assert spider_boy.id is not None + assert spider_boy.secret_name == "Pedro Parqueador" + assert rusty_man.name == "Rusty-Man" + assert rusty_man.age == 48 + assert rusty_man.id is not None + assert rusty_man.secret_name == "Tommy Sharp" diff --git a/tests/test_tutorial/test_insert/test_tutorial002_py310.py b/tests/test_tutorial/test_insert/test_tutorial002_py310.py new file mode 100644 index 0000000000..fb62810baf --- /dev/null +++ b/tests/test_tutorial/test_insert/test_tutorial002_py310.py @@ -0,0 +1,30 @@ +from sqlmodel import Session, create_engine, select + +from ...conftest import needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.insert import tutorial002_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + mod.main() + with Session(mod.engine) as session: + heroes = session.exec(select(mod.Hero)).all() + heroes_by_name = {hero.name: hero for hero in heroes} + deadpond = heroes_by_name["Deadpond"] + spider_boy = heroes_by_name["Spider-Boy"] + rusty_man = heroes_by_name["Rusty-Man"] + assert deadpond.name == "Deadpond" + assert deadpond.age is None + assert deadpond.id is not None + assert deadpond.secret_name == "Dive Wilson" + assert spider_boy.name == "Spider-Boy" + assert spider_boy.age is None + assert spider_boy.id is not None + assert spider_boy.secret_name == "Pedro Parqueador" + assert rusty_man.name == "Rusty-Man" + assert rusty_man.age == 48 + assert rusty_man.id is not None + assert rusty_man.secret_name == "Tommy Sharp" diff --git a/tests/test_tutorial/test_insert/test_tutorial003_py310.py b/tests/test_tutorial/test_insert/test_tutorial003_py310.py new file mode 100644 index 0000000000..5bca713e60 --- /dev/null +++ b/tests/test_tutorial/test_insert/test_tutorial003_py310.py @@ -0,0 +1,30 @@ +from sqlmodel import Session, create_engine, select + +from ...conftest import needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.insert import tutorial003_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + mod.main() + with Session(mod.engine) as session: + heroes = session.exec(select(mod.Hero)).all() + heroes_by_name = {hero.name: hero for hero in heroes} + deadpond = heroes_by_name["Deadpond"] + spider_boy = heroes_by_name["Spider-Boy"] + rusty_man = heroes_by_name["Rusty-Man"] + assert deadpond.name == "Deadpond" + assert deadpond.age is None + assert deadpond.id is not None + assert deadpond.secret_name == "Dive Wilson" + assert spider_boy.name == "Spider-Boy" + assert spider_boy.age is None + assert spider_boy.id is not None + assert spider_boy.secret_name == "Pedro Parqueador" + assert rusty_man.name == "Rusty-Man" + assert rusty_man.age == 48 + assert rusty_man.id is not None + assert rusty_man.secret_name == "Tommy Sharp" diff --git a/tests/test_tutorial/test_limit_and_offset/test_tutorial001_py310.py b/tests/test_tutorial/test_limit_and_offset/test_tutorial001_py310.py new file mode 100644 index 0000000000..4f4974c853 --- /dev/null +++ b/tests/test_tutorial/test_limit_and_offset/test_tutorial001_py310.py @@ -0,0 +1,35 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + +expected_calls = [ + [ + [ + {"id": 1, "name": "Deadpond", "secret_name": "Dive Wilson", "age": None}, + { + "id": 2, + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "age": None, + }, + {"id": 3, "name": "Rusty-Man", "secret_name": "Tommy Sharp", "age": 48}, + ] + ] +] + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.offset_and_limit import tutorial001_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_limit_and_offset/test_tutorial002_py310.py b/tests/test_tutorial/test_limit_and_offset/test_tutorial002_py310.py new file mode 100644 index 0000000000..1f86d1960e --- /dev/null +++ b/tests/test_tutorial/test_limit_and_offset/test_tutorial002_py310.py @@ -0,0 +1,35 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + +expected_calls = [ + [ + [ + { + "id": 4, + "name": "Tarantula", + "secret_name": "Natalia Roman-on", + "age": 32, + }, + {"id": 5, "name": "Black Lion", "secret_name": "Trevor Challa", "age": 35}, + {"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}, + ] + ] +] + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.offset_and_limit import tutorial002_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_limit_and_offset/test_tutorial003_py310.py b/tests/test_tutorial/test_limit_and_offset/test_tutorial003_py310.py new file mode 100644 index 0000000000..993999156d --- /dev/null +++ b/tests/test_tutorial/test_limit_and_offset/test_tutorial003_py310.py @@ -0,0 +1,33 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + +expected_calls = [ + [ + [ + { + "id": 7, + "name": "Captain North America", + "secret_name": "Esteban Rogelios", + "age": 93, + } + ] + ] +] + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.offset_and_limit import tutorial003_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_limit_and_offset/test_tutorial004_py310.py b/tests/test_tutorial/test_limit_and_offset/test_tutorial004_py310.py new file mode 100644 index 0000000000..4ca736589f --- /dev/null +++ b/tests/test_tutorial/test_limit_and_offset/test_tutorial004_py310.py @@ -0,0 +1,27 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.offset_and_limit import tutorial004_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == [ + [ + [ + {"name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36, "id": 6}, + {"name": "Rusty-Man", "secret_name": "Tommy Sharp", "age": 48, "id": 3}, + ] + ] + ] diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial001_py310.py b/tests/test_tutorial/test_many_to_many/test_tutorial001_py310.py new file mode 100644 index 0000000000..19a1640bd6 --- /dev/null +++ b/tests/test_tutorial/test_many_to_many/test_tutorial001_py310.py @@ -0,0 +1,50 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + +expected_calls = [ + [ + "Deadpond:", + {"id": 1, "secret_name": "Dive Wilson", "age": None, "name": "Deadpond"}, + ], + [ + "Deadpond teams:", + [ + {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret’s Bar"}, + {"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}, + ], + ], + [ + "Rusty-Man:", + {"id": 2, "secret_name": "Tommy Sharp", "age": 48, "name": "Rusty-Man"}, + ], + [ + "Rusty-Man Teams:", + [{"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}], + ], + [ + "Spider-Boy:", + {"id": 3, "secret_name": "Pedro Parqueador", "age": None, "name": "Spider-Boy"}, + ], + [ + "Spider-Boy Teams:", + [{"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}], + ], +] + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.many_to_many import tutorial001_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial001_py39.py b/tests/test_tutorial/test_many_to_many/test_tutorial001_py39.py new file mode 100644 index 0000000000..23a7649f03 --- /dev/null +++ b/tests/test_tutorial/test_many_to_many/test_tutorial001_py39.py @@ -0,0 +1,50 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py39 + +expected_calls = [ + [ + "Deadpond:", + {"id": 1, "secret_name": "Dive Wilson", "age": None, "name": "Deadpond"}, + ], + [ + "Deadpond teams:", + [ + {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret’s Bar"}, + {"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}, + ], + ], + [ + "Rusty-Man:", + {"id": 2, "secret_name": "Tommy Sharp", "age": 48, "name": "Rusty-Man"}, + ], + [ + "Rusty-Man Teams:", + [{"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}], + ], + [ + "Spider-Boy:", + {"id": 3, "secret_name": "Pedro Parqueador", "age": None, "name": "Spider-Boy"}, + ], + [ + "Spider-Boy Teams:", + [{"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}], + ], +] + + +@needs_py39 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.many_to_many import tutorial001_py39 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial002_py310.py b/tests/test_tutorial/test_many_to_many/test_tutorial002_py310.py new file mode 100644 index 0000000000..d480190f6f --- /dev/null +++ b/tests/test_tutorial/test_many_to_many/test_tutorial002_py310.py @@ -0,0 +1,77 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + +expected_calls = [ + [ + "Deadpond:", + {"id": 1, "secret_name": "Dive Wilson", "age": None, "name": "Deadpond"}, + ], + [ + "Deadpond teams:", + [ + {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret’s Bar"}, + {"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}, + ], + ], + [ + "Rusty-Man:", + {"id": 2, "secret_name": "Tommy Sharp", "age": 48, "name": "Rusty-Man"}, + ], + [ + "Rusty-Man Teams:", + [{"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}], + ], + [ + "Spider-Boy:", + {"id": 3, "secret_name": "Pedro Parqueador", "age": None, "name": "Spider-Boy"}, + ], + [ + "Spider-Boy Teams:", + [{"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}], + ], + [ + "Updated Spider-Boy's Teams:", + [ + {"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}, + {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret’s Bar"}, + ], + ], + [ + "Z-Force heroes:", + [ + {"id": 1, "secret_name": "Dive Wilson", "age": None, "name": "Deadpond"}, + { + "id": 3, + "secret_name": "Pedro Parqueador", + "age": None, + "name": "Spider-Boy", + }, + ], + ], + [ + "Reverted Z-Force's heroes:", + [{"id": 1, "secret_name": "Dive Wilson", "age": None, "name": "Deadpond"}], + ], + [ + "Reverted Spider-Boy's teams:", + [{"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}], + ], +] + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.many_to_many import tutorial002_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial002_py39.py b/tests/test_tutorial/test_many_to_many/test_tutorial002_py39.py new file mode 100644 index 0000000000..85f649acdf --- /dev/null +++ b/tests/test_tutorial/test_many_to_many/test_tutorial002_py39.py @@ -0,0 +1,77 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py39 + +expected_calls = [ + [ + "Deadpond:", + {"id": 1, "secret_name": "Dive Wilson", "age": None, "name": "Deadpond"}, + ], + [ + "Deadpond teams:", + [ + {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret’s Bar"}, + {"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}, + ], + ], + [ + "Rusty-Man:", + {"id": 2, "secret_name": "Tommy Sharp", "age": 48, "name": "Rusty-Man"}, + ], + [ + "Rusty-Man Teams:", + [{"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}], + ], + [ + "Spider-Boy:", + {"id": 3, "secret_name": "Pedro Parqueador", "age": None, "name": "Spider-Boy"}, + ], + [ + "Spider-Boy Teams:", + [{"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}], + ], + [ + "Updated Spider-Boy's Teams:", + [ + {"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}, + {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret’s Bar"}, + ], + ], + [ + "Z-Force heroes:", + [ + {"id": 1, "secret_name": "Dive Wilson", "age": None, "name": "Deadpond"}, + { + "id": 3, + "secret_name": "Pedro Parqueador", + "age": None, + "name": "Spider-Boy", + }, + ], + ], + [ + "Reverted Z-Force's heroes:", + [{"id": 1, "secret_name": "Dive Wilson", "age": None, "name": "Deadpond"}], + ], + [ + "Reverted Spider-Boy's teams:", + [{"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}], + ], +] + + +@needs_py39 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.many_to_many import tutorial002_py39 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial003_py310.py b/tests/test_tutorial/test_many_to_many/test_tutorial003_py310.py new file mode 100644 index 0000000000..29918b3584 --- /dev/null +++ b/tests/test_tutorial/test_many_to_many/test_tutorial003_py310.py @@ -0,0 +1,73 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + +expected_calls = [ + [ + "Z-Force hero:", + {"name": "Deadpond", "secret_name": "Dive Wilson", "id": 1, "age": None}, + "is training:", + False, + ], + [ + "Preventers hero:", + {"name": "Deadpond", "secret_name": "Dive Wilson", "id": 1, "age": None}, + "is training:", + True, + ], + [ + "Preventers hero:", + {"name": "Spider-Boy", "secret_name": "Pedro Parqueador", "id": 2, "age": None}, + "is training:", + True, + ], + [ + "Preventers hero:", + {"name": "Rusty-Man", "secret_name": "Tommy Sharp", "id": 3, "age": 48}, + "is training:", + False, + ], + [ + "Updated Spider-Boy's Teams:", + [ + {"team_id": 2, "is_training": True, "hero_id": 2}, + {"team_id": 1, "is_training": True, "hero_id": 2}, + ], + ], + [ + "Z-Force heroes:", + [ + {"team_id": 1, "is_training": False, "hero_id": 1}, + {"team_id": 1, "is_training": True, "hero_id": 2}, + ], + ], + [ + "Spider-Boy team:", + {"headquarters": "Sharp Tower", "id": 2, "name": "Preventers"}, + "is training:", + False, + ], + [ + "Spider-Boy team:", + {"headquarters": "Sister Margaret’s Bar", "id": 1, "name": "Z-Force"}, + "is training:", + True, + ], +] + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.many_to_many import tutorial003_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial003_py39.py b/tests/test_tutorial/test_many_to_many/test_tutorial003_py39.py new file mode 100644 index 0000000000..5f1c87ddc3 --- /dev/null +++ b/tests/test_tutorial/test_many_to_many/test_tutorial003_py39.py @@ -0,0 +1,73 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py39 + +expected_calls = [ + [ + "Z-Force hero:", + {"name": "Deadpond", "secret_name": "Dive Wilson", "id": 1, "age": None}, + "is training:", + False, + ], + [ + "Preventers hero:", + {"name": "Deadpond", "secret_name": "Dive Wilson", "id": 1, "age": None}, + "is training:", + True, + ], + [ + "Preventers hero:", + {"name": "Spider-Boy", "secret_name": "Pedro Parqueador", "id": 2, "age": None}, + "is training:", + True, + ], + [ + "Preventers hero:", + {"name": "Rusty-Man", "secret_name": "Tommy Sharp", "id": 3, "age": 48}, + "is training:", + False, + ], + [ + "Updated Spider-Boy's Teams:", + [ + {"team_id": 2, "is_training": True, "hero_id": 2}, + {"team_id": 1, "is_training": True, "hero_id": 2}, + ], + ], + [ + "Z-Force heroes:", + [ + {"team_id": 1, "is_training": False, "hero_id": 1}, + {"team_id": 1, "is_training": True, "hero_id": 2}, + ], + ], + [ + "Spider-Boy team:", + {"headquarters": "Sharp Tower", "id": 2, "name": "Preventers"}, + "is training:", + False, + ], + [ + "Spider-Boy team:", + {"headquarters": "Sister Margaret’s Bar", "id": 1, "name": "Z-Force"}, + "is training:", + True, + ], +] + + +@needs_py39 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.many_to_many import tutorial003_py39 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_one/test_tutorial001_py310.py b/tests/test_tutorial/test_one/test_tutorial001_py310.py new file mode 100644 index 0000000000..6de878087f --- /dev/null +++ b/tests/test_tutorial/test_one/test_tutorial001_py310.py @@ -0,0 +1,30 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.one import tutorial001_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == [ + [ + "Hero:", + { + "name": "Tarantula", + "secret_name": "Natalia Roman-on", + "age": 32, + "id": 4, + }, + ] + ] diff --git a/tests/test_tutorial/test_one/test_tutorial002_py310.py b/tests/test_tutorial/test_one/test_tutorial002_py310.py new file mode 100644 index 0000000000..afdfc54593 --- /dev/null +++ b/tests/test_tutorial/test_one/test_tutorial002_py310.py @@ -0,0 +1,20 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.one import tutorial002_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == [["Hero:", None]] diff --git a/tests/test_tutorial/test_one/test_tutorial003_py310.py b/tests/test_tutorial/test_one/test_tutorial003_py310.py new file mode 100644 index 0000000000..8eb8b8612b --- /dev/null +++ b/tests/test_tutorial/test_one/test_tutorial003_py310.py @@ -0,0 +1,25 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.one import tutorial003_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == [ + [ + "Hero:", + {"name": "Deadpond", "secret_name": "Dive Wilson", "age": None, "id": 1}, + ] + ] diff --git a/tests/test_tutorial/test_one/test_tutorial004_py310.py b/tests/test_tutorial/test_one/test_tutorial004_py310.py new file mode 100644 index 0000000000..cf365a4fe5 --- /dev/null +++ b/tests/test_tutorial/test_one/test_tutorial004_py310.py @@ -0,0 +1,41 @@ +from unittest.mock import patch + +import pytest +from sqlalchemy.exc import MultipleResultsFound +from sqlmodel import Session, create_engine, delete + +from ...conftest import get_testing_print_function, needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.one import tutorial004_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + with pytest.raises(MultipleResultsFound): + mod.main() + with Session(mod.engine) as session: + # TODO: create delete() function + # TODO: add overloads for .exec() with delete object + session.exec(delete(mod.Hero)) + session.add(mod.Hero(name="Test Hero", secret_name="Secret Test Hero", age=24)) + session.commit() + + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.select_heroes() + assert calls == [ + [ + "Hero:", + { + "id": 1, + "name": "Test Hero", + "secret_name": "Secret Test Hero", + "age": 24, + }, + ] + ] diff --git a/tests/test_tutorial/test_one/test_tutorial005_py310.py b/tests/test_tutorial/test_one/test_tutorial005_py310.py new file mode 100644 index 0000000000..f1fce7d764 --- /dev/null +++ b/tests/test_tutorial/test_one/test_tutorial005_py310.py @@ -0,0 +1,41 @@ +from unittest.mock import patch + +import pytest +from sqlalchemy.exc import NoResultFound +from sqlmodel import Session, create_engine, delete + +from ...conftest import get_testing_print_function, needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.one import tutorial005_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + with pytest.raises(NoResultFound): + mod.main() + with Session(mod.engine) as session: + # TODO: create delete() function + # TODO: add overloads for .exec() with delete object + session.exec(delete(mod.Hero)) + session.add(mod.Hero(name="Test Hero", secret_name="Secret Test Hero", age=24)) + session.commit() + + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.select_heroes() + assert calls == [ + [ + "Hero:", + { + "id": 1, + "name": "Test Hero", + "secret_name": "Secret Test Hero", + "age": 24, + }, + ] + ] diff --git a/tests/test_tutorial/test_one/test_tutorial006_py310.py b/tests/test_tutorial/test_one/test_tutorial006_py310.py new file mode 100644 index 0000000000..ad8577c7ae --- /dev/null +++ b/tests/test_tutorial/test_one/test_tutorial006_py310.py @@ -0,0 +1,25 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.one import tutorial006_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == [ + [ + "Hero:", + {"name": "Deadpond", "secret_name": "Dive Wilson", "age": None, "id": 1}, + ] + ] diff --git a/tests/test_tutorial/test_one/test_tutorial007_py310.py b/tests/test_tutorial/test_one/test_tutorial007_py310.py new file mode 100644 index 0000000000..15b2306fc6 --- /dev/null +++ b/tests/test_tutorial/test_one/test_tutorial007_py310.py @@ -0,0 +1,25 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.one import tutorial007_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == [ + [ + "Hero:", + {"name": "Deadpond", "secret_name": "Dive Wilson", "age": None, "id": 1}, + ] + ] diff --git a/tests/test_tutorial/test_one/test_tutorial008_py310.py b/tests/test_tutorial/test_one/test_tutorial008_py310.py new file mode 100644 index 0000000000..c7d1fe55c9 --- /dev/null +++ b/tests/test_tutorial/test_one/test_tutorial008_py310.py @@ -0,0 +1,25 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.one import tutorial008_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == [ + [ + "Hero:", + {"name": "Deadpond", "secret_name": "Dive Wilson", "age": None, "id": 1}, + ] + ] diff --git a/tests/test_tutorial/test_one/test_tutorial009_py310.py b/tests/test_tutorial/test_one/test_tutorial009_py310.py new file mode 100644 index 0000000000..8e9fda5f73 --- /dev/null +++ b/tests/test_tutorial/test_one/test_tutorial009_py310.py @@ -0,0 +1,20 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.one import tutorial009_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == [["Hero:", None]] diff --git a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001_py310.py b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001_py310.py new file mode 100644 index 0000000000..384056ad7b --- /dev/null +++ b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001_py310.py @@ -0,0 +1,290 @@ +from unittest.mock import patch + +import pytest +from sqlalchemy.exc import SAWarning +from sqlmodel import create_engine + +from ....conftest import get_testing_print_function, needs_py310 + +expected_calls = [ + [ + "Created hero:", + { + "age": None, + "id": 1, + "secret_name": "Dive Wilson", + "team_id": 1, + "name": "Deadpond", + }, + ], + [ + "Created hero:", + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 2, + "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": 2, + "name": "Spider-Boy", + }, + ], + [ + "Team Wakaland:", + {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, + ], + [ + "Preventers new hero:", + { + "age": 32, + "id": 6, + "secret_name": "Natalia Roman-on", + "team_id": 2, + "name": "Tarantula", + }, + ], + [ + "Preventers new hero:", + { + "age": 36, + "id": 7, + "secret_name": "Steve Weird", + "team_id": 2, + "name": "Dr. Weird", + }, + ], + [ + "Preventers new hero:", + { + "age": 93, + "id": 8, + "secret_name": "Esteban Rogelios", + "team_id": 2, + "name": "Captain North America", + }, + ], + [ + "Preventers heroes:", + [ + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 2, + "name": "Rusty-Man", + }, + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": 2, + "name": "Spider-Boy", + }, + { + "age": 32, + "id": 6, + "secret_name": "Natalia Roman-on", + "team_id": 2, + "name": "Tarantula", + }, + { + "age": 36, + "id": 7, + "secret_name": "Steve Weird", + "team_id": 2, + "name": "Dr. Weird", + }, + { + "age": 93, + "id": 8, + "secret_name": "Esteban Rogelios", + "team_id": 2, + "name": "Captain North America", + }, + ], + ], + [ + "Hero Spider-Boy:", + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": 2, + "name": "Spider-Boy", + }, + ], + [ + "Preventers Team:", + {"headquarters": "Sharp Tower", "id": 2, "name": "Preventers"}, + ], + [ + "Preventers Team Heroes:", + [ + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 2, + "name": "Rusty-Man", + }, + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": 2, + "name": "Spider-Boy", + }, + { + "age": 32, + "id": 6, + "secret_name": "Natalia Roman-on", + "team_id": 2, + "name": "Tarantula", + }, + { + "age": 36, + "id": 7, + "secret_name": "Steve Weird", + "team_id": 2, + "name": "Dr. Weird", + }, + { + "age": 93, + "id": 8, + "secret_name": "Esteban Rogelios", + "team_id": 2, + "name": "Captain North America", + }, + ], + ], + [ + "Spider-Boy without team:", + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": 2, + "name": "Spider-Boy", + }, + ], + [ + "Preventers Team Heroes again:", + [ + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 2, + "name": "Rusty-Man", + }, + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": 2, + "name": "Spider-Boy", + }, + { + "age": 32, + "id": 6, + "secret_name": "Natalia Roman-on", + "team_id": 2, + "name": "Tarantula", + }, + { + "age": 36, + "id": 7, + "secret_name": "Steve Weird", + "team_id": 2, + "name": "Dr. Weird", + }, + { + "age": 93, + "id": 8, + "secret_name": "Esteban Rogelios", + "team_id": 2, + "name": "Captain North America", + }, + ], + ], + ["After committing"], + [ + "Spider-Boy after commit:", + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": None, + "name": "Spider-Boy", + }, + ], + [ + "Preventers Team Heroes after commit:", + [ + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 2, + "name": "Rusty-Man", + }, + { + "age": 32, + "id": 6, + "secret_name": "Natalia Roman-on", + "team_id": 2, + "name": "Tarantula", + }, + { + "age": 36, + "id": 7, + "secret_name": "Steve Weird", + "team_id": 2, + "name": "Dr. Weird", + }, + { + "age": 93, + "id": 8, + "secret_name": "Esteban Rogelios", + "team_id": 2, + "name": "Captain North America", + }, + ], + ], +] + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.relationship_attributes.back_populates import ( + tutorial001_py310 as mod, + ) + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + with pytest.warns(SAWarning): + mod.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001_py39.py b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001_py39.py new file mode 100644 index 0000000000..0597a88e89 --- /dev/null +++ b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001_py39.py @@ -0,0 +1,290 @@ +from unittest.mock import patch + +import pytest +from sqlalchemy.exc import SAWarning +from sqlmodel import create_engine + +from ....conftest import get_testing_print_function, needs_py39 + +expected_calls = [ + [ + "Created hero:", + { + "age": None, + "id": 1, + "secret_name": "Dive Wilson", + "team_id": 1, + "name": "Deadpond", + }, + ], + [ + "Created hero:", + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 2, + "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": 2, + "name": "Spider-Boy", + }, + ], + [ + "Team Wakaland:", + {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, + ], + [ + "Preventers new hero:", + { + "age": 32, + "id": 6, + "secret_name": "Natalia Roman-on", + "team_id": 2, + "name": "Tarantula", + }, + ], + [ + "Preventers new hero:", + { + "age": 36, + "id": 7, + "secret_name": "Steve Weird", + "team_id": 2, + "name": "Dr. Weird", + }, + ], + [ + "Preventers new hero:", + { + "age": 93, + "id": 8, + "secret_name": "Esteban Rogelios", + "team_id": 2, + "name": "Captain North America", + }, + ], + [ + "Preventers heroes:", + [ + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 2, + "name": "Rusty-Man", + }, + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": 2, + "name": "Spider-Boy", + }, + { + "age": 32, + "id": 6, + "secret_name": "Natalia Roman-on", + "team_id": 2, + "name": "Tarantula", + }, + { + "age": 36, + "id": 7, + "secret_name": "Steve Weird", + "team_id": 2, + "name": "Dr. Weird", + }, + { + "age": 93, + "id": 8, + "secret_name": "Esteban Rogelios", + "team_id": 2, + "name": "Captain North America", + }, + ], + ], + [ + "Hero Spider-Boy:", + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": 2, + "name": "Spider-Boy", + }, + ], + [ + "Preventers Team:", + {"headquarters": "Sharp Tower", "id": 2, "name": "Preventers"}, + ], + [ + "Preventers Team Heroes:", + [ + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 2, + "name": "Rusty-Man", + }, + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": 2, + "name": "Spider-Boy", + }, + { + "age": 32, + "id": 6, + "secret_name": "Natalia Roman-on", + "team_id": 2, + "name": "Tarantula", + }, + { + "age": 36, + "id": 7, + "secret_name": "Steve Weird", + "team_id": 2, + "name": "Dr. Weird", + }, + { + "age": 93, + "id": 8, + "secret_name": "Esteban Rogelios", + "team_id": 2, + "name": "Captain North America", + }, + ], + ], + [ + "Spider-Boy without team:", + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": 2, + "name": "Spider-Boy", + }, + ], + [ + "Preventers Team Heroes again:", + [ + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 2, + "name": "Rusty-Man", + }, + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": 2, + "name": "Spider-Boy", + }, + { + "age": 32, + "id": 6, + "secret_name": "Natalia Roman-on", + "team_id": 2, + "name": "Tarantula", + }, + { + "age": 36, + "id": 7, + "secret_name": "Steve Weird", + "team_id": 2, + "name": "Dr. Weird", + }, + { + "age": 93, + "id": 8, + "secret_name": "Esteban Rogelios", + "team_id": 2, + "name": "Captain North America", + }, + ], + ], + ["After committing"], + [ + "Spider-Boy after commit:", + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": None, + "name": "Spider-Boy", + }, + ], + [ + "Preventers Team Heroes after commit:", + [ + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 2, + "name": "Rusty-Man", + }, + { + "age": 32, + "id": 6, + "secret_name": "Natalia Roman-on", + "team_id": 2, + "name": "Tarantula", + }, + { + "age": 36, + "id": 7, + "secret_name": "Steve Weird", + "team_id": 2, + "name": "Dr. Weird", + }, + { + "age": 93, + "id": 8, + "secret_name": "Esteban Rogelios", + "team_id": 2, + "name": "Captain North America", + }, + ], + ], +] + + +@needs_py39 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.relationship_attributes.back_populates import ( + tutorial001_py39 as mod, + ) + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + with pytest.warns(SAWarning): + mod.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002_py310.py b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002_py310.py new file mode 100644 index 0000000000..50a891f310 --- /dev/null +++ b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002_py310.py @@ -0,0 +1,280 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ....conftest import get_testing_print_function, needs_py310 + +expected_calls = [ + [ + "Created hero:", + { + "age": None, + "id": 1, + "secret_name": "Dive Wilson", + "team_id": 1, + "name": "Deadpond", + }, + ], + [ + "Created hero:", + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 2, + "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": 2, + "name": "Spider-Boy", + }, + ], + [ + "Team Wakaland:", + {"id": 3, "name": "Wakaland", "headquarters": "Wakaland Capital City"}, + ], + [ + "Preventers new hero:", + { + "age": 32, + "id": 6, + "secret_name": "Natalia Roman-on", + "team_id": 2, + "name": "Tarantula", + }, + ], + [ + "Preventers new hero:", + { + "age": 36, + "id": 7, + "secret_name": "Steve Weird", + "team_id": 2, + "name": "Dr. Weird", + }, + ], + [ + "Preventers new hero:", + { + "age": 93, + "id": 8, + "secret_name": "Esteban Rogelios", + "team_id": 2, + "name": "Captain North America", + }, + ], + [ + "Preventers heroes:", + [ + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 2, + "name": "Rusty-Man", + }, + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": 2, + "name": "Spider-Boy", + }, + { + "age": 32, + "id": 6, + "secret_name": "Natalia Roman-on", + "team_id": 2, + "name": "Tarantula", + }, + { + "age": 36, + "id": 7, + "secret_name": "Steve Weird", + "team_id": 2, + "name": "Dr. Weird", + }, + { + "age": 93, + "id": 8, + "secret_name": "Esteban Rogelios", + "team_id": 2, + "name": "Captain North America", + }, + ], + ], + [ + "Hero Spider-Boy:", + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": 2, + "name": "Spider-Boy", + }, + ], + [ + "Preventers Team:", + {"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}, + ], + [ + "Preventers Team Heroes:", + [ + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 2, + "name": "Rusty-Man", + }, + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": 2, + "name": "Spider-Boy", + }, + { + "age": 32, + "id": 6, + "secret_name": "Natalia Roman-on", + "team_id": 2, + "name": "Tarantula", + }, + { + "age": 36, + "id": 7, + "secret_name": "Steve Weird", + "team_id": 2, + "name": "Dr. Weird", + }, + { + "age": 93, + "id": 8, + "secret_name": "Esteban Rogelios", + "team_id": 2, + "name": "Captain North America", + }, + ], + ], + [ + "Spider-Boy without team:", + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": 2, + "name": "Spider-Boy", + }, + ], + [ + "Preventers Team Heroes again:", + [ + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 2, + "name": "Rusty-Man", + }, + { + "age": 32, + "id": 6, + "secret_name": "Natalia Roman-on", + "team_id": 2, + "name": "Tarantula", + }, + { + "age": 36, + "id": 7, + "secret_name": "Steve Weird", + "team_id": 2, + "name": "Dr. Weird", + }, + { + "age": 93, + "id": 8, + "secret_name": "Esteban Rogelios", + "team_id": 2, + "name": "Captain North America", + }, + ], + ], + ["After committing"], + [ + "Spider-Boy after commit:", + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": None, + "name": "Spider-Boy", + }, + ], + [ + "Preventers Team Heroes after commit:", + [ + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 2, + "name": "Rusty-Man", + }, + { + "age": 32, + "id": 6, + "secret_name": "Natalia Roman-on", + "team_id": 2, + "name": "Tarantula", + }, + { + "age": 36, + "id": 7, + "secret_name": "Steve Weird", + "team_id": 2, + "name": "Dr. Weird", + }, + { + "age": 93, + "id": 8, + "secret_name": "Esteban Rogelios", + "team_id": 2, + "name": "Captain North America", + }, + ], + ], +] + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.relationship_attributes.back_populates import ( + tutorial002_py310 as mod, + ) + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002_py39.py b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002_py39.py new file mode 100644 index 0000000000..3da6ce4aac --- /dev/null +++ b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002_py39.py @@ -0,0 +1,280 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ....conftest import get_testing_print_function, needs_py39 + +expected_calls = [ + [ + "Created hero:", + { + "age": None, + "id": 1, + "secret_name": "Dive Wilson", + "team_id": 1, + "name": "Deadpond", + }, + ], + [ + "Created hero:", + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 2, + "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": 2, + "name": "Spider-Boy", + }, + ], + [ + "Team Wakaland:", + {"id": 3, "name": "Wakaland", "headquarters": "Wakaland Capital City"}, + ], + [ + "Preventers new hero:", + { + "age": 32, + "id": 6, + "secret_name": "Natalia Roman-on", + "team_id": 2, + "name": "Tarantula", + }, + ], + [ + "Preventers new hero:", + { + "age": 36, + "id": 7, + "secret_name": "Steve Weird", + "team_id": 2, + "name": "Dr. Weird", + }, + ], + [ + "Preventers new hero:", + { + "age": 93, + "id": 8, + "secret_name": "Esteban Rogelios", + "team_id": 2, + "name": "Captain North America", + }, + ], + [ + "Preventers heroes:", + [ + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 2, + "name": "Rusty-Man", + }, + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": 2, + "name": "Spider-Boy", + }, + { + "age": 32, + "id": 6, + "secret_name": "Natalia Roman-on", + "team_id": 2, + "name": "Tarantula", + }, + { + "age": 36, + "id": 7, + "secret_name": "Steve Weird", + "team_id": 2, + "name": "Dr. Weird", + }, + { + "age": 93, + "id": 8, + "secret_name": "Esteban Rogelios", + "team_id": 2, + "name": "Captain North America", + }, + ], + ], + [ + "Hero Spider-Boy:", + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": 2, + "name": "Spider-Boy", + }, + ], + [ + "Preventers Team:", + {"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}, + ], + [ + "Preventers Team Heroes:", + [ + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 2, + "name": "Rusty-Man", + }, + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": 2, + "name": "Spider-Boy", + }, + { + "age": 32, + "id": 6, + "secret_name": "Natalia Roman-on", + "team_id": 2, + "name": "Tarantula", + }, + { + "age": 36, + "id": 7, + "secret_name": "Steve Weird", + "team_id": 2, + "name": "Dr. Weird", + }, + { + "age": 93, + "id": 8, + "secret_name": "Esteban Rogelios", + "team_id": 2, + "name": "Captain North America", + }, + ], + ], + [ + "Spider-Boy without team:", + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": 2, + "name": "Spider-Boy", + }, + ], + [ + "Preventers Team Heroes again:", + [ + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 2, + "name": "Rusty-Man", + }, + { + "age": 32, + "id": 6, + "secret_name": "Natalia Roman-on", + "team_id": 2, + "name": "Tarantula", + }, + { + "age": 36, + "id": 7, + "secret_name": "Steve Weird", + "team_id": 2, + "name": "Dr. Weird", + }, + { + "age": 93, + "id": 8, + "secret_name": "Esteban Rogelios", + "team_id": 2, + "name": "Captain North America", + }, + ], + ], + ["After committing"], + [ + "Spider-Boy after commit:", + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": None, + "name": "Spider-Boy", + }, + ], + [ + "Preventers Team Heroes after commit:", + [ + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 2, + "name": "Rusty-Man", + }, + { + "age": 32, + "id": 6, + "secret_name": "Natalia Roman-on", + "team_id": 2, + "name": "Tarantula", + }, + { + "age": 36, + "id": 7, + "secret_name": "Steve Weird", + "team_id": 2, + "name": "Dr. Weird", + }, + { + "age": 93, + "id": 8, + "secret_name": "Esteban Rogelios", + "team_id": 2, + "name": "Captain North America", + }, + ], + ], +] + + +@needs_py39 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.relationship_attributes.back_populates import ( + tutorial002_py39 as mod, + ) + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003_py310.py b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003_py310.py new file mode 100644 index 0000000000..82e0c1c03b --- /dev/null +++ b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003_py310.py @@ -0,0 +1,21 @@ +from sqlalchemy import inspect +from sqlalchemy.engine.reflection import Inspector +from sqlmodel import create_engine + +from ....conftest import needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.relationship_attributes.back_populates import ( + tutorial003_py310 as mod, + ) + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + mod.main() + insp: Inspector = inspect(mod.engine) + assert insp.has_table(str(mod.Hero.__tablename__)) + assert insp.has_table(str(mod.Weapon.__tablename__)) + assert insp.has_table(str(mod.Power.__tablename__)) + assert insp.has_table(str(mod.Team.__tablename__)) diff --git a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003_py39.py b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003_py39.py new file mode 100644 index 0000000000..d6059cb485 --- /dev/null +++ b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003_py39.py @@ -0,0 +1,21 @@ +from sqlalchemy import inspect +from sqlalchemy.engine.reflection import Inspector +from sqlmodel import create_engine + +from ....conftest import needs_py39 + + +@needs_py39 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.relationship_attributes.back_populates import ( + tutorial003_py39 as mod, + ) + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + mod.main() + insp: Inspector = inspect(mod.engine) + assert insp.has_table(str(mod.Hero.__tablename__)) + assert insp.has_table(str(mod.Weapon.__tablename__)) + assert insp.has_table(str(mod.Power.__tablename__)) + assert insp.has_table(str(mod.Team.__tablename__)) diff --git a/tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001_py310.py b/tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001_py310.py new file mode 100644 index 0000000000..c239b6d55c --- /dev/null +++ b/tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001_py310.py @@ -0,0 +1,99 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ....conftest import get_testing_print_function, needs_py310 + +expected_calls = [ + [ + "Created hero:", + { + "age": None, + "id": 1, + "secret_name": "Dive Wilson", + "team_id": 1, + "name": "Deadpond", + }, + ], + [ + "Created hero:", + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 2, + "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": 2, + "name": "Spider-Boy", + }, + ], + [ + "Team Wakaland:", + {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, + ], + [ + "Preventers new hero:", + { + "age": 32, + "id": 6, + "secret_name": "Natalia Roman-on", + "team_id": 2, + "name": "Tarantula", + }, + ], + [ + "Preventers new hero:", + { + "age": 36, + "id": 7, + "secret_name": "Steve Weird", + "team_id": 2, + "name": "Dr. Weird", + }, + ], + [ + "Preventers new hero:", + { + "age": 93, + "id": 8, + "secret_name": "Esteban Rogelios", + "team_id": 2, + "name": "Captain North America", + }, + ], +] + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.relationship_attributes.create_and_update_relationships import ( + tutorial001_py310 as mod, + ) + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001_py39.py b/tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001_py39.py new file mode 100644 index 0000000000..c569eed0d5 --- /dev/null +++ b/tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001_py39.py @@ -0,0 +1,99 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ....conftest import get_testing_print_function, needs_py39 + +expected_calls = [ + [ + "Created hero:", + { + "age": None, + "id": 1, + "secret_name": "Dive Wilson", + "team_id": 1, + "name": "Deadpond", + }, + ], + [ + "Created hero:", + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 2, + "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": 2, + "name": "Spider-Boy", + }, + ], + [ + "Team Wakaland:", + {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, + ], + [ + "Preventers new hero:", + { + "age": 32, + "id": 6, + "secret_name": "Natalia Roman-on", + "team_id": 2, + "name": "Tarantula", + }, + ], + [ + "Preventers new hero:", + { + "age": 36, + "id": 7, + "secret_name": "Steve Weird", + "team_id": 2, + "name": "Dr. Weird", + }, + ], + [ + "Preventers new hero:", + { + "age": 93, + "id": 8, + "secret_name": "Esteban Rogelios", + "team_id": 2, + "name": "Captain North America", + }, + ], +] + + +@needs_py39 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.relationship_attributes.create_and_update_relationships import ( + tutorial001_py39 as mod, + ) + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001_py310.py b/tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001_py310.py new file mode 100644 index 0000000000..f595dcaa04 --- /dev/null +++ b/tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001_py310.py @@ -0,0 +1,55 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ....conftest import get_testing_print_function, needs_py310 + +expected_calls = [ + [ + "Created hero:", + { + "name": "Deadpond", + "age": None, + "team_id": 1, + "id": 1, + "secret_name": "Dive Wilson", + }, + ], + [ + "Created hero:", + { + "name": "Rusty-Man", + "age": 48, + "team_id": 2, + "id": 2, + "secret_name": "Tommy Sharp", + }, + ], + [ + "Created hero:", + { + "name": "Spider-Boy", + "age": None, + "team_id": None, + "id": 3, + "secret_name": "Pedro Parqueador", + }, + ], +] + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.relationship_attributes.define_relationship_attributes import ( + tutorial001_py310 as mod, + ) + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001_py39.py b/tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001_py39.py new file mode 100644 index 0000000000..d54c610d19 --- /dev/null +++ b/tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001_py39.py @@ -0,0 +1,55 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ....conftest import get_testing_print_function, needs_py39 + +expected_calls = [ + [ + "Created hero:", + { + "name": "Deadpond", + "age": None, + "team_id": 1, + "id": 1, + "secret_name": "Dive Wilson", + }, + ], + [ + "Created hero:", + { + "name": "Rusty-Man", + "age": 48, + "team_id": 2, + "id": 2, + "secret_name": "Tommy Sharp", + }, + ], + [ + "Created hero:", + { + "name": "Spider-Boy", + "age": None, + "team_id": None, + "id": 3, + "secret_name": "Pedro Parqueador", + }, + ], +] + + +@needs_py39 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.relationship_attributes.define_relationship_attributes import ( + tutorial001_py39 as mod, + ) + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001_py310.py b/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001_py310.py new file mode 100644 index 0000000000..9a4e3cc53b --- /dev/null +++ b/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001_py310.py @@ -0,0 +1,107 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ....conftest import get_testing_print_function, needs_py310 + +expected_calls = [ + [ + "Created hero:", + { + "age": None, + "id": 1, + "secret_name": "Dive Wilson", + "team_id": 1, + "name": "Deadpond", + }, + ], + [ + "Created hero:", + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 2, + "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": 2, + "name": "Spider-Boy", + }, + ], + [ + "Team Wakaland:", + {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, + ], + [ + "Preventers new hero:", + { + "age": 32, + "id": 6, + "secret_name": "Natalia Roman-on", + "team_id": 2, + "name": "Tarantula", + }, + ], + [ + "Preventers new hero:", + { + "age": 36, + "id": 7, + "secret_name": "Steve Weird", + "team_id": 2, + "name": "Dr. Weird", + }, + ], + [ + "Preventers new hero:", + { + "age": 93, + "id": 8, + "secret_name": "Esteban Rogelios", + "team_id": 2, + "name": "Captain North America", + }, + ], + [ + "Spider-Boy's team:", + {"headquarters": "Sharp Tower", "id": 2, "name": "Preventers"}, + ], + [ + "Spider-Boy's team again:", + {"headquarters": "Sharp Tower", "id": 2, "name": "Preventers"}, + ], +] + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.relationship_attributes.read_relationships import ( + tutorial001_py310 as mod, + ) + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001_py39.py b/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001_py39.py new file mode 100644 index 0000000000..6b23980665 --- /dev/null +++ b/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001_py39.py @@ -0,0 +1,107 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ....conftest import get_testing_print_function, needs_py39 + +expected_calls = [ + [ + "Created hero:", + { + "age": None, + "id": 1, + "secret_name": "Dive Wilson", + "team_id": 1, + "name": "Deadpond", + }, + ], + [ + "Created hero:", + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 2, + "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": 2, + "name": "Spider-Boy", + }, + ], + [ + "Team Wakaland:", + {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, + ], + [ + "Preventers new hero:", + { + "age": 32, + "id": 6, + "secret_name": "Natalia Roman-on", + "team_id": 2, + "name": "Tarantula", + }, + ], + [ + "Preventers new hero:", + { + "age": 36, + "id": 7, + "secret_name": "Steve Weird", + "team_id": 2, + "name": "Dr. Weird", + }, + ], + [ + "Preventers new hero:", + { + "age": 93, + "id": 8, + "secret_name": "Esteban Rogelios", + "team_id": 2, + "name": "Captain North America", + }, + ], + [ + "Spider-Boy's team:", + {"headquarters": "Sharp Tower", "id": 2, "name": "Preventers"}, + ], + [ + "Spider-Boy's team again:", + {"headquarters": "Sharp Tower", "id": 2, "name": "Preventers"}, + ], +] + + +@needs_py39 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.relationship_attributes.read_relationships import ( + tutorial001_py39 as mod, + ) + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002_py310.py b/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002_py310.py new file mode 100644 index 0000000000..0cc9ae3326 --- /dev/null +++ b/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002_py310.py @@ -0,0 +1,149 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ....conftest import get_testing_print_function, needs_py310 + +expected_calls = [ + [ + "Created hero:", + { + "age": None, + "id": 1, + "secret_name": "Dive Wilson", + "team_id": 1, + "name": "Deadpond", + }, + ], + [ + "Created hero:", + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 2, + "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": 2, + "name": "Spider-Boy", + }, + ], + [ + "Team Wakaland:", + {"id": 3, "name": "Wakaland", "headquarters": "Wakaland Capital City"}, + ], + [ + "Preventers new hero:", + { + "age": 32, + "id": 6, + "secret_name": "Natalia Roman-on", + "team_id": 2, + "name": "Tarantula", + }, + ], + [ + "Preventers new hero:", + { + "age": 36, + "id": 7, + "secret_name": "Steve Weird", + "team_id": 2, + "name": "Dr. Weird", + }, + ], + [ + "Preventers new hero:", + { + "age": 93, + "id": 8, + "secret_name": "Esteban Rogelios", + "team_id": 2, + "name": "Captain North America", + }, + ], + [ + "Preventers heroes:", + [ + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 2, + "name": "Rusty-Man", + }, + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": 2, + "name": "Spider-Boy", + }, + { + "age": 32, + "id": 6, + "secret_name": "Natalia Roman-on", + "team_id": 2, + "name": "Tarantula", + }, + { + "age": 36, + "id": 7, + "secret_name": "Steve Weird", + "team_id": 2, + "name": "Dr. Weird", + }, + { + "age": 93, + "id": 8, + "secret_name": "Esteban Rogelios", + "team_id": 2, + "name": "Captain North America", + }, + ], + ], + [ + "Spider-Boy without team:", + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": None, + "name": "Spider-Boy", + }, + ], +] + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.relationship_attributes.read_relationships import ( + tutorial002_py310 as mod, + ) + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002_py39.py b/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002_py39.py new file mode 100644 index 0000000000..891f4ca6a9 --- /dev/null +++ b/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002_py39.py @@ -0,0 +1,149 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ....conftest import get_testing_print_function, needs_py39 + +expected_calls = [ + [ + "Created hero:", + { + "age": None, + "id": 1, + "secret_name": "Dive Wilson", + "team_id": 1, + "name": "Deadpond", + }, + ], + [ + "Created hero:", + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 2, + "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": 2, + "name": "Spider-Boy", + }, + ], + [ + "Team Wakaland:", + {"id": 3, "name": "Wakaland", "headquarters": "Wakaland Capital City"}, + ], + [ + "Preventers new hero:", + { + "age": 32, + "id": 6, + "secret_name": "Natalia Roman-on", + "team_id": 2, + "name": "Tarantula", + }, + ], + [ + "Preventers new hero:", + { + "age": 36, + "id": 7, + "secret_name": "Steve Weird", + "team_id": 2, + "name": "Dr. Weird", + }, + ], + [ + "Preventers new hero:", + { + "age": 93, + "id": 8, + "secret_name": "Esteban Rogelios", + "team_id": 2, + "name": "Captain North America", + }, + ], + [ + "Preventers heroes:", + [ + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 2, + "name": "Rusty-Man", + }, + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": 2, + "name": "Spider-Boy", + }, + { + "age": 32, + "id": 6, + "secret_name": "Natalia Roman-on", + "team_id": 2, + "name": "Tarantula", + }, + { + "age": 36, + "id": 7, + "secret_name": "Steve Weird", + "team_id": 2, + "name": "Dr. Weird", + }, + { + "age": 93, + "id": 8, + "secret_name": "Esteban Rogelios", + "team_id": 2, + "name": "Captain North America", + }, + ], + ], + [ + "Spider-Boy without team:", + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": None, + "name": "Spider-Boy", + }, + ], +] + + +@needs_py39 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.relationship_attributes.read_relationships import ( + tutorial002_py39 as mod, + ) + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_select/test_tutorial001_py310_tutorial002_py310.py b/tests/test_tutorial/test_select/test_tutorial001_py310_tutorial002_py310.py new file mode 100644 index 0000000000..7521b6b717 --- /dev/null +++ b/tests/test_tutorial/test_select/test_tutorial001_py310_tutorial002_py310.py @@ -0,0 +1,57 @@ +from typing import Any, Dict, List, Union +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + + +def check_calls(calls: List[List[Union[str, Dict[str, Any]]]]): + 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, + } + + +@needs_py310 +def test_tutorial_001(clear_sqlmodel): + from docs_src.tutorial.select import tutorial001_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + check_calls(calls) + + +@needs_py310 +def test_tutorial_002(clear_sqlmodel): + from docs_src.tutorial.select import tutorial002_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + check_calls(calls) diff --git a/tests/test_tutorial/test_select/test_tutorial003_py310_tutorial004_py310.py b/tests/test_tutorial/test_select/test_tutorial003_py310_tutorial004_py310.py new file mode 100644 index 0000000000..0fa69df4a1 --- /dev/null +++ b/tests/test_tutorial/test_select/test_tutorial003_py310_tutorial004_py310.py @@ -0,0 +1,59 @@ +from typing import Any, Dict, List, Union +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + + +def check_calls(calls: List[List[Union[str, Dict[str, Any]]]]): + assert calls[0][0] == [ + { + "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, + }, + ] + + +@needs_py310 +def test_tutorial_003(clear_sqlmodel): + from docs_src.tutorial.select import tutorial003_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + check_calls(calls) + + +@needs_py310 +def test_tutorial_002(clear_sqlmodel): + from docs_src.tutorial.select import tutorial004_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + check_calls(calls) diff --git a/tests/test_tutorial/test_update/test_tutorial001_py310_tutorial002_py310.py b/tests/test_tutorial/test_update/test_tutorial001_py310_tutorial002_py310.py new file mode 100644 index 0000000000..cefb75f333 --- /dev/null +++ b/tests/test_tutorial/test_update/test_tutorial001_py310_tutorial002_py310.py @@ -0,0 +1,56 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + +expected_calls = [ + [ + "Hero:", + { + "id": 2, + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "age": None, + }, + ], + [ + "Updated hero:", + { + "id": 2, + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "age": 16, + }, + ], +] + + +@needs_py310 +def test_tutorial001(clear_sqlmodel): + from docs_src.tutorial.update import tutorial001_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls + + +@needs_py310 +def test_tutorial002(clear_sqlmodel): + from docs_src.tutorial.update import tutorial002_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_update/test_tutorial003_py310_tutorial004_py310.py b/tests/test_tutorial/test_update/test_tutorial003_py310_tutorial004_py310.py new file mode 100644 index 0000000000..31dc601901 --- /dev/null +++ b/tests/test_tutorial/test_update/test_tutorial003_py310_tutorial004_py310.py @@ -0,0 +1,69 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + +expected_calls = [ + [ + "Hero 1:", + {"id": 2, "name": "Spider-Boy", "secret_name": "Pedro Parqueador", "age": None}, + ], + [ + "Hero 2:", + { + "id": 7, + "name": "Captain North America", + "secret_name": "Esteban Rogelios", + "age": 93, + }, + ], + [ + "Updated hero 1:", + { + "id": 2, + "name": "Spider-Youngster", + "secret_name": "Pedro Parqueador", + "age": 16, + }, + ], + [ + "Updated hero 2:", + { + "id": 7, + "name": "Captain North America Except Canada", + "secret_name": "Esteban Rogelios", + "age": 110, + }, + ], +] + + +@needs_py310 +def test_tutorial003(clear_sqlmodel): + from docs_src.tutorial.update import tutorial003_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls + + +@needs_py310 +def test_tutorial004(clear_sqlmodel): + from docs_src.tutorial.update import tutorial004_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == expected_calls diff --git a/tests/test_tutorial/test_where/test_tutorial001_py310.py b/tests/test_tutorial/test_where/test_tutorial001_py310.py new file mode 100644 index 0000000000..44e734ad7d --- /dev/null +++ b/tests/test_tutorial/test_where/test_tutorial001_py310.py @@ -0,0 +1,29 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.where import tutorial001_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == [ + [ + { + "name": "Deadpond", + "secret_name": "Dive Wilson", + "age": None, + "id": 1, + } + ] + ] diff --git a/tests/test_tutorial/test_where/test_tutorial002_py310.py b/tests/test_tutorial/test_where/test_tutorial002_py310.py new file mode 100644 index 0000000000..00d88ecdde --- /dev/null +++ b/tests/test_tutorial/test_where/test_tutorial002_py310.py @@ -0,0 +1,30 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.where import tutorial002_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == [ + [ + { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "age": None, + "id": 2, + } + ], + [{"name": "Rusty-Man", "secret_name": "Tommy Sharp", "age": 48, "id": 3}], + ] diff --git a/tests/test_tutorial/test_where/test_tutorial003_py310.py b/tests/test_tutorial/test_where/test_tutorial003_py310.py new file mode 100644 index 0000000000..2d84c2ca82 --- /dev/null +++ b/tests/test_tutorial/test_where/test_tutorial003_py310.py @@ -0,0 +1,37 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.where import tutorial003_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + + expected_calls = [ + [{"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}], + [{"id": 3, "name": "Rusty-Man", "secret_name": "Tommy Sharp", "age": 48}], + [ + { + "id": 7, + "name": "Captain North America", + "secret_name": "Esteban Rogelios", + "age": 93, + } + ], + ] + for call in expected_calls: + assert call in calls, "This expected item should be in the list" + # Now that this item was checked, remove it from the list + calls.pop(calls.index(call)) + assert len(calls) == 0, "The list should only have the expected items" diff --git a/tests/test_tutorial/test_where/test_tutorial004_py310.py b/tests/test_tutorial/test_where/test_tutorial004_py310.py new file mode 100644 index 0000000000..04566cbbec --- /dev/null +++ b/tests/test_tutorial/test_where/test_tutorial004_py310.py @@ -0,0 +1,37 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.where import tutorial004_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + expected_calls = [ + [{"id": 5, "name": "Black Lion", "secret_name": "Trevor Challa", "age": 35}], + [{"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}], + [{"id": 3, "name": "Rusty-Man", "secret_name": "Tommy Sharp", "age": 48}], + [ + { + "id": 7, + "name": "Captain North America", + "secret_name": "Esteban Rogelios", + "age": 93, + } + ], + ] + for call in expected_calls: + assert call in calls, "This expected item should be in the list" + # Now that this item was checked, remove it from the list + calls.pop(calls.index(call)) + assert len(calls) == 0, "The list should only have the expected items" diff --git a/tests/test_tutorial/test_where/test_tutorial005_py310.py b/tests/test_tutorial/test_where/test_tutorial005_py310.py new file mode 100644 index 0000000000..d238fff4f8 --- /dev/null +++ b/tests/test_tutorial/test_where/test_tutorial005_py310.py @@ -0,0 +1,22 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.where import tutorial005_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == [ + [{"name": "Tarantula", "secret_name": "Natalia Roman-on", "age": 32, "id": 4}] + ] diff --git a/tests/test_tutorial/test_where/test_tutorial006_py310.py b/tests/test_tutorial/test_where/test_tutorial006_py310.py new file mode 100644 index 0000000000..8a4924fc09 --- /dev/null +++ b/tests/test_tutorial/test_where/test_tutorial006_py310.py @@ -0,0 +1,23 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.where import tutorial006_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == [ + [{"name": "Tarantula", "secret_name": "Natalia Roman-on", "age": 32, "id": 4}], + [{"name": "Black Lion", "secret_name": "Trevor Challa", "age": 35, "id": 5}], + ] diff --git a/tests/test_tutorial/test_where/test_tutorial007_py310.py b/tests/test_tutorial/test_where/test_tutorial007_py310.py new file mode 100644 index 0000000000..a2110a19dc --- /dev/null +++ b/tests/test_tutorial/test_where/test_tutorial007_py310.py @@ -0,0 +1,23 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.where import tutorial007_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == [ + [{"id": 5, "name": "Black Lion", "secret_name": "Trevor Challa", "age": 35}], + [{"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}], + ] diff --git a/tests/test_tutorial/test_where/test_tutorial008_py310.py b/tests/test_tutorial/test_where/test_tutorial008_py310.py new file mode 100644 index 0000000000..887ac70abd --- /dev/null +++ b/tests/test_tutorial/test_where/test_tutorial008_py310.py @@ -0,0 +1,23 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.where import tutorial008_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == [ + [{"id": 5, "name": "Black Lion", "secret_name": "Trevor Challa", "age": 35}], + [{"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}], + ] diff --git a/tests/test_tutorial/test_where/test_tutorial009_py310.py b/tests/test_tutorial/test_where/test_tutorial009_py310.py new file mode 100644 index 0000000000..9bbef9b9f8 --- /dev/null +++ b/tests/test_tutorial/test_where/test_tutorial009_py310.py @@ -0,0 +1,31 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.where import tutorial009_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == [ + [{"name": "Tarantula", "secret_name": "Natalia Roman-on", "age": 32, "id": 4}], + [{"name": "Black Lion", "secret_name": "Trevor Challa", "age": 35, "id": 5}], + [ + { + "name": "Captain North America", + "secret_name": "Esteban Rogelios", + "age": 93, + "id": 7, + } + ], + ] diff --git a/tests/test_tutorial/test_where/test_tutorial010_py310.py b/tests/test_tutorial/test_where/test_tutorial010_py310.py new file mode 100644 index 0000000000..e990abed44 --- /dev/null +++ b/tests/test_tutorial/test_where/test_tutorial010_py310.py @@ -0,0 +1,31 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.where import tutorial010_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == [ + [{"name": "Tarantula", "secret_name": "Natalia Roman-on", "age": 32, "id": 4}], + [{"name": "Black Lion", "secret_name": "Trevor Challa", "age": 35, "id": 5}], + [ + { + "name": "Captain North America", + "secret_name": "Esteban Rogelios", + "age": 93, + "id": 7, + } + ], + ] diff --git a/tests/test_tutorial/test_where/test_tutorial011_py310.py b/tests/test_tutorial/test_where/test_tutorial011_py310.py new file mode 100644 index 0000000000..aee809b15b --- /dev/null +++ b/tests/test_tutorial/test_where/test_tutorial011_py310.py @@ -0,0 +1,37 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.where import tutorial011_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + expected_calls = [ + [{"id": 5, "name": "Black Lion", "secret_name": "Trevor Challa", "age": 35}], + [{"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}], + [{"id": 3, "name": "Rusty-Man", "secret_name": "Tommy Sharp", "age": 48}], + [ + { + "id": 7, + "name": "Captain North America", + "secret_name": "Esteban Rogelios", + "age": 93, + } + ], + ] + for call in expected_calls: + assert call in calls, "This expected item should be in the list" + # Now that this item was checked, remove it from the list + calls.pop(calls.index(call)) + assert len(calls) == 0, "The list should only have the expected items" From 98739b071c1d8fd8cbc591e1467f3629c0c2259f Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 29 Nov 2023 15:52:16 +0000 Subject: [PATCH 310/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index b2661a3578..43b8d9ebc4 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -13,6 +13,10 @@ * ⬆️ Add support for Python 3.11 and Python 3.12. PR [#710](https://github.com/tiangolo/sqlmodel/pull/710) by [@tiangolo](https://github.com/tiangolo). +### Docs + +* 📝 Add source examples for Python 3.9 and 3.10. PR [#715](https://github.com/tiangolo/sqlmodel/pull/715) by [@tiangolo](https://github.com/tiangolo). + ### Internal * 🔧 Show line numbers in docs during local development. PR [#714](https://github.com/tiangolo/sqlmodel/pull/714) by [@tiangolo](https://github.com/tiangolo). From f42314956f78f902248b165f1108f62bb3f323e0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 30 Nov 2023 16:19:47 +0100 Subject: [PATCH 311/906] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#697)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.4 → v0.1.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.4...v0.1.6) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6e84cce283..b21e5ac9cc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.4 + rev: v0.1.6 hooks: - id: ruff args: From 2ecc86275ff4e5491db887859a7312a919fd5423 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 30 Nov 2023 15:20:09 +0000 Subject: [PATCH 312/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 43b8d9ebc4..2792a9f9f5 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#697](https://github.com/tiangolo/sqlmodel/pull/697) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * 🔧 Show line numbers in docs during local development. PR [#714](https://github.com/tiangolo/sqlmodel/pull/714) by [@tiangolo](https://github.com/tiangolo). * 📝 Update details syntax with new pymdown extensions format. PR [#713](https://github.com/tiangolo/sqlmodel/pull/713) by [@tiangolo](https://github.com/tiangolo). From 4ac87146b1dc7ffb0c5c8cc2e8c3744b9a5f940e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 30 Nov 2023 16:23:06 +0100 Subject: [PATCH 313/906] =?UTF-8?q?=F0=9F=94=87=20Do=20not=20raise=20depre?= =?UTF-8?q?cation=20warnings=20for=20execute=20as=20it's=20automatically?= =?UTF-8?q?=20used=20internally=20(#716)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔇 Do not raise deprecation warnings for execute as it's automatically used internally * ✅ Tweak tests to not use deprecated query --- sqlmodel/orm/session.py | 3 ++- tests/test_main.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/sqlmodel/orm/session.py b/sqlmodel/orm/session.py index 6050d5fbc1..e404bb137d 100644 --- a/sqlmodel/orm/session.py +++ b/sqlmodel/orm/session.py @@ -95,7 +95,8 @@ def exec( ```Python heroes = session.exec(select(Hero)).all() ``` - """ + """, + category=None, ) def execute( # type: ignore self, diff --git a/tests/test_main.py b/tests/test_main.py index 72465cda33..bdbcdeb76d 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -3,7 +3,7 @@ import pytest from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import RelationshipProperty -from sqlmodel import Field, Relationship, Session, SQLModel, create_engine +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select def test_should_allow_duplicate_row_if_unique_constraint_is_not_passed(clear_sqlmodel): @@ -31,7 +31,7 @@ class Hero(SQLModel, table=True): session.refresh(hero_2) with Session(engine) as session: - heroes = session.query(Hero).all() + heroes = session.exec(select(Hero)).all() assert len(heroes) == 2 assert heroes[0].name == heroes[1].name @@ -61,7 +61,7 @@ class Hero(SQLModel, table=True): session.refresh(hero_2) with Session(engine) as session: - heroes = session.query(Hero).all() + heroes = session.exec(select(Hero)).all() assert len(heroes) == 2 assert heroes[0].name == heroes[1].name From e5fddb97a780d34758331611a8266be1bbb10c5e Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 30 Nov 2023 15:23:25 +0000 Subject: [PATCH 314/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 2792a9f9f5..18521f487c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,6 +7,7 @@ ### Refactors +* 🔇 Do not raise deprecation warnings for execute as it's automatically used internally. PR [#716](https://github.com/tiangolo/sqlmodel/pull/716) by [@tiangolo](https://github.com/tiangolo). * ✅ Move OpenAPI tests inline to simplify updating them with Pydantic v2. PR [#709](https://github.com/tiangolo/sqlmodel/pull/709) by [@tiangolo](https://github.com/tiangolo). ### Upgrades From 853d787923b74b9eb4f1c74a156d2c8652910fce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 4 Dec 2023 10:46:59 +0100 Subject: [PATCH 315/906] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20type=20?= =?UTF-8?q?generation=20of=20selects=20re-order=20to=20prioritize=20models?= =?UTF-8?q?=20to=20optimize=20editor=20support=20(#718)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/generate_select.py | 2 +- sqlmodel/sql/expression.py | 164 +++++++++++++++--------------- sqlmodel/sql/expression.py.jinja2 | 4 +- 3 files changed, 85 insertions(+), 85 deletions(-) diff --git a/scripts/generate_select.py b/scripts/generate_select.py index 88e0e0a997..8f01cb808d 100644 --- a/scripts/generate_select.py +++ b/scripts/generate_select.py @@ -24,7 +24,7 @@ class Arg(BaseModel): signatures: List[Tuple[List[Arg], List[str]]] = [] for total_args in range(2, number_of_types + 1): - arg_types_tuples = product(["scalar", "model"], repeat=total_args) + arg_types_tuples = product(["model", "scalar"], repeat=total_args) for arg_type_tuple in arg_types_tuples: args: List[Arg] = [] return_types: List[str] = [] diff --git a/sqlmodel/sql/expression.py b/sqlmodel/sql/expression.py index a8a572501c..2c931a1479 100644 --- a/sqlmodel/sql/expression.py +++ b/sqlmodel/sql/expression.py @@ -328,12 +328,12 @@ class SelectOfScalar(SelectBase[_T]): @overload -def select(__ent0: _TScalar_0) -> SelectOfScalar[_TScalar_0]: # type: ignore +def select(__ent0: _TCCA[_T0]) -> SelectOfScalar[_T0]: ... @overload -def select(__ent0: _TCCA[_T0]) -> SelectOfScalar[_T0]: +def select(__ent0: _TScalar_0) -> SelectOfScalar[_TScalar_0]: # type: ignore ... @@ -342,17 +342,9 @@ def select(__ent0: _TCCA[_T0]) -> SelectOfScalar[_T0]: @overload def select( # type: ignore - entity_0: _TScalar_0, - entity_1: _TScalar_1, -) -> Select[Tuple[_TScalar_0, _TScalar_1]]: - ... - - -@overload -def select( # type: ignore - entity_0: _TScalar_0, + __ent0: _TCCA[_T0], __ent1: _TCCA[_T1], -) -> Select[Tuple[_TScalar_0, _T1]]: +) -> Select[Tuple[_T0, _T1]]: ... @@ -366,9 +358,9 @@ def select( # type: ignore @overload def select( # type: ignore - __ent0: _TCCA[_T0], + entity_0: _TScalar_0, __ent1: _TCCA[_T1], -) -> Select[Tuple[_T0, _T1]]: +) -> Select[Tuple[_TScalar_0, _T1]]: ... @@ -376,35 +368,34 @@ def select( # type: ignore def select( # type: ignore entity_0: _TScalar_0, entity_1: _TScalar_1, - entity_2: _TScalar_2, -) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2]]: +) -> Select[Tuple[_TScalar_0, _TScalar_1]]: ... @overload def select( # type: ignore - entity_0: _TScalar_0, - entity_1: _TScalar_1, + __ent0: _TCCA[_T0], + __ent1: _TCCA[_T1], __ent2: _TCCA[_T2], -) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2]]: +) -> Select[Tuple[_T0, _T1, _T2]]: ... @overload def select( # type: ignore - entity_0: _TScalar_0, + __ent0: _TCCA[_T0], __ent1: _TCCA[_T1], entity_2: _TScalar_2, -) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2]]: +) -> Select[Tuple[_T0, _T1, _TScalar_2]]: ... @overload def select( # type: ignore - entity_0: _TScalar_0, - __ent1: _TCCA[_T1], + __ent0: _TCCA[_T0], + entity_1: _TScalar_1, __ent2: _TCCA[_T2], -) -> Select[Tuple[_TScalar_0, _T1, _T2]]: +) -> Select[Tuple[_T0, _TScalar_1, _T2]]: ... @@ -419,28 +410,28 @@ def select( # type: ignore @overload def select( # type: ignore - __ent0: _TCCA[_T0], - entity_1: _TScalar_1, + entity_0: _TScalar_0, + __ent1: _TCCA[_T1], __ent2: _TCCA[_T2], -) -> Select[Tuple[_T0, _TScalar_1, _T2]]: +) -> Select[Tuple[_TScalar_0, _T1, _T2]]: ... @overload def select( # type: ignore - __ent0: _TCCA[_T0], + entity_0: _TScalar_0, __ent1: _TCCA[_T1], entity_2: _TScalar_2, -) -> Select[Tuple[_T0, _T1, _TScalar_2]]: +) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2]]: ... @overload def select( # type: ignore - __ent0: _TCCA[_T0], - __ent1: _TCCA[_T1], + entity_0: _TScalar_0, + entity_1: _TScalar_1, __ent2: _TCCA[_T2], -) -> Select[Tuple[_T0, _T1, _T2]]: +) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2]]: ... @@ -449,78 +440,77 @@ def select( # type: ignore entity_0: _TScalar_0, entity_1: _TScalar_1, entity_2: _TScalar_2, - entity_3: _TScalar_3, -) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2, _TScalar_3]]: +) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2]]: ... @overload def select( # type: ignore - entity_0: _TScalar_0, - entity_1: _TScalar_1, - entity_2: _TScalar_2, + __ent0: _TCCA[_T0], + __ent1: _TCCA[_T1], + __ent2: _TCCA[_T2], __ent3: _TCCA[_T3], -) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2, _T3]]: +) -> Select[Tuple[_T0, _T1, _T2, _T3]]: ... @overload def select( # type: ignore - entity_0: _TScalar_0, - entity_1: _TScalar_1, + __ent0: _TCCA[_T0], + __ent1: _TCCA[_T1], __ent2: _TCCA[_T2], entity_3: _TScalar_3, -) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2, _TScalar_3]]: +) -> Select[Tuple[_T0, _T1, _T2, _TScalar_3]]: ... @overload def select( # type: ignore - entity_0: _TScalar_0, - entity_1: _TScalar_1, - __ent2: _TCCA[_T2], + __ent0: _TCCA[_T0], + __ent1: _TCCA[_T1], + entity_2: _TScalar_2, __ent3: _TCCA[_T3], -) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2, _T3]]: +) -> Select[Tuple[_T0, _T1, _TScalar_2, _T3]]: ... @overload def select( # type: ignore - entity_0: _TScalar_0, + __ent0: _TCCA[_T0], __ent1: _TCCA[_T1], entity_2: _TScalar_2, entity_3: _TScalar_3, -) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2, _TScalar_3]]: +) -> Select[Tuple[_T0, _T1, _TScalar_2, _TScalar_3]]: ... @overload def select( # type: ignore - entity_0: _TScalar_0, - __ent1: _TCCA[_T1], - entity_2: _TScalar_2, + __ent0: _TCCA[_T0], + entity_1: _TScalar_1, + __ent2: _TCCA[_T2], __ent3: _TCCA[_T3], -) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2, _T3]]: +) -> Select[Tuple[_T0, _TScalar_1, _T2, _T3]]: ... @overload def select( # type: ignore - entity_0: _TScalar_0, - __ent1: _TCCA[_T1], + __ent0: _TCCA[_T0], + entity_1: _TScalar_1, __ent2: _TCCA[_T2], entity_3: _TScalar_3, -) -> Select[Tuple[_TScalar_0, _T1, _T2, _TScalar_3]]: +) -> Select[Tuple[_T0, _TScalar_1, _T2, _TScalar_3]]: ... @overload def select( # type: ignore - entity_0: _TScalar_0, - __ent1: _TCCA[_T1], - __ent2: _TCCA[_T2], + __ent0: _TCCA[_T0], + entity_1: _TScalar_1, + entity_2: _TScalar_2, __ent3: _TCCA[_T3], -) -> Select[Tuple[_TScalar_0, _T1, _T2, _T3]]: +) -> Select[Tuple[_T0, _TScalar_1, _TScalar_2, _T3]]: ... @@ -536,71 +526,81 @@ def select( # type: ignore @overload def select( # type: ignore - __ent0: _TCCA[_T0], - entity_1: _TScalar_1, - entity_2: _TScalar_2, + entity_0: _TScalar_0, + __ent1: _TCCA[_T1], + __ent2: _TCCA[_T2], __ent3: _TCCA[_T3], -) -> Select[Tuple[_T0, _TScalar_1, _TScalar_2, _T3]]: +) -> Select[Tuple[_TScalar_0, _T1, _T2, _T3]]: ... @overload def select( # type: ignore - __ent0: _TCCA[_T0], - entity_1: _TScalar_1, + entity_0: _TScalar_0, + __ent1: _TCCA[_T1], __ent2: _TCCA[_T2], entity_3: _TScalar_3, -) -> Select[Tuple[_T0, _TScalar_1, _T2, _TScalar_3]]: +) -> Select[Tuple[_TScalar_0, _T1, _T2, _TScalar_3]]: ... @overload def select( # type: ignore - __ent0: _TCCA[_T0], - entity_1: _TScalar_1, - __ent2: _TCCA[_T2], + entity_0: _TScalar_0, + __ent1: _TCCA[_T1], + entity_2: _TScalar_2, __ent3: _TCCA[_T3], -) -> Select[Tuple[_T0, _TScalar_1, _T2, _T3]]: +) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2, _T3]]: ... @overload def select( # type: ignore - __ent0: _TCCA[_T0], + entity_0: _TScalar_0, __ent1: _TCCA[_T1], entity_2: _TScalar_2, entity_3: _TScalar_3, -) -> Select[Tuple[_T0, _T1, _TScalar_2, _TScalar_3]]: +) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2, _TScalar_3]]: ... @overload def select( # type: ignore - __ent0: _TCCA[_T0], - __ent1: _TCCA[_T1], - entity_2: _TScalar_2, + entity_0: _TScalar_0, + entity_1: _TScalar_1, + __ent2: _TCCA[_T2], __ent3: _TCCA[_T3], -) -> Select[Tuple[_T0, _T1, _TScalar_2, _T3]]: +) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2, _T3]]: ... @overload def select( # type: ignore - __ent0: _TCCA[_T0], - __ent1: _TCCA[_T1], + entity_0: _TScalar_0, + entity_1: _TScalar_1, __ent2: _TCCA[_T2], entity_3: _TScalar_3, -) -> Select[Tuple[_T0, _T1, _T2, _TScalar_3]]: +) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2, _TScalar_3]]: ... @overload def select( # type: ignore - __ent0: _TCCA[_T0], - __ent1: _TCCA[_T1], - __ent2: _TCCA[_T2], + entity_0: _TScalar_0, + entity_1: _TScalar_1, + entity_2: _TScalar_2, __ent3: _TCCA[_T3], -) -> Select[Tuple[_T0, _T1, _T2, _T3]]: +) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2, _T3]]: + ... + + +@overload +def select( # type: ignore + entity_0: _TScalar_0, + entity_1: _TScalar_1, + entity_2: _TScalar_2, + entity_3: _TScalar_3, +) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2, _TScalar_3]]: ... diff --git a/sqlmodel/sql/expression.py.jinja2 b/sqlmodel/sql/expression.py.jinja2 index f1a25419c0..9bf93e1abf 100644 --- a/sqlmodel/sql/expression.py.jinja2 +++ b/sqlmodel/sql/expression.py.jinja2 @@ -273,12 +273,12 @@ _T{{ i }} = TypeVar("_T{{ i }}") # Generated TypeVars end @overload -def select(__ent0: _TScalar_0) -> SelectOfScalar[_TScalar_0]: # type: ignore +def select(__ent0: _TCCA[_T0]) -> SelectOfScalar[_T0]: ... @overload -def select(__ent0: _TCCA[_T0]) -> SelectOfScalar[_T0]: +def select(__ent0: _TScalar_0) -> SelectOfScalar[_TScalar_0]: # type: ignore ... From 50b0198423880790d0ef76979ab05e3f77f952ec Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 4 Dec 2023 09:47:19 +0000 Subject: [PATCH 316/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 18521f487c..789509fd61 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -5,6 +5,10 @@ * 🔧 Update config with new pymdown extensions. PR [#712](https://github.com/tiangolo/sqlmodel/pull/712) by [@tiangolo](https://github.com/tiangolo). * 🙈 Update gitignore, include all coverage files. PR [#711](https://github.com/tiangolo/sqlmodel/pull/711) by [@tiangolo](https://github.com/tiangolo). +### Fixes + +* ♻️ Refactor type generation of selects re-order to prioritize models to optimize editor support. PR [#718](https://github.com/tiangolo/sqlmodel/pull/718) by [@tiangolo](https://github.com/tiangolo). + ### Refactors * 🔇 Do not raise deprecation warnings for execute as it's automatically used internally. PR [#716](https://github.com/tiangolo/sqlmodel/pull/716) by [@tiangolo](https://github.com/tiangolo). From 41495e30c7509b2d23b6673017b3d2b14a36b26b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 4 Dec 2023 10:49:23 +0100 Subject: [PATCH 317/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20docs=20for=20De?= =?UTF-8?q?cimal,=20use=20proper=20types=20(#719)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/advanced/decimal.md | 14 +++----------- docs_src/advanced/decimal/tutorial001.py | 4 ++-- docs_src/advanced/decimal/tutorial001_py310.py | 5 +++-- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/docs/advanced/decimal.md b/docs/advanced/decimal.md index 036aae0003..2b58550d7f 100644 --- a/docs/advanced/decimal.md +++ b/docs/advanced/decimal.md @@ -19,21 +19,13 @@ In most cases this would probably not be a problem, for example measuring views ## Decimal Types -Pydantic has special support for `Decimal` types using the `condecimal()` special function. +Pydantic has special support for `Decimal` types. -/// tip - -Pydantic 1.9, that will be released soon, has improved support for `Decimal` types, without needing to use the `condecimal()` function. - -But meanwhile, you can already use this feature with `condecimal()` in **SQLModel** it as it's explained here. - -/// - -When you use `condecimal()` you can specify the number of digits and decimal places to support. They will be validated by Pydantic (for example when using FastAPI) and the same information will also be used for the database columns. +When you use `Decimal` you can specify the number of digits and decimal places to support in the `Field()` function. They will be validated by Pydantic (for example when using FastAPI) and the same information will also be used for the database columns. /// info -For the database, **SQLModel** will use SQLAlchemy's `DECIMAL` type. +For the database, **SQLModel** will use SQLAlchemy's `DECIMAL` type. /// diff --git a/docs_src/advanced/decimal/tutorial001.py b/docs_src/advanced/decimal/tutorial001.py index b803119d9e..a0a9804ade 100644 --- a/docs_src/advanced/decimal/tutorial001.py +++ b/docs_src/advanced/decimal/tutorial001.py @@ -1,6 +1,6 @@ +from decimal import Decimal from typing import Optional -from pydantic import condecimal from sqlmodel import Field, Session, SQLModel, create_engine, select @@ -9,7 +9,7 @@ class Hero(SQLModel, table=True): name: str = Field(index=True) secret_name: str age: Optional[int] = Field(default=None, index=True) - money: condecimal(max_digits=5, decimal_places=3) = Field(default=0) + money: Decimal = Field(default=0, max_digits=5, decimal_places=3) sqlite_file_name = "database.db" diff --git a/docs_src/advanced/decimal/tutorial001_py310.py b/docs_src/advanced/decimal/tutorial001_py310.py index 92afc09012..267338912e 100644 --- a/docs_src/advanced/decimal/tutorial001_py310.py +++ b/docs_src/advanced/decimal/tutorial001_py310.py @@ -1,4 +1,5 @@ -from pydantic import condecimal +from decimal import Decimal + from sqlmodel import Field, Session, SQLModel, create_engine, select @@ -7,7 +8,7 @@ class Hero(SQLModel, table=True): name: str = Field(index=True) secret_name: str age: int | None = Field(default=None, index=True) - money: condecimal(max_digits=5, decimal_places=3) = Field(default=0) + money: Decimal = Field(default=0, max_digits=5, decimal_places=3) sqlite_file_name = "database.db" From cc11619c67eb9e76528f349b51b4c31fd7d71d6a Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 4 Dec 2023 09:49:42 +0000 Subject: [PATCH 318/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 789509fd61..bbac758c68 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -20,6 +20,7 @@ ### Docs +* 📝 Update docs for Decimal, use proper types. PR [#719](https://github.com/tiangolo/sqlmodel/pull/719) by [@tiangolo](https://github.com/tiangolo). * 📝 Add source examples for Python 3.9 and 3.10. PR [#715](https://github.com/tiangolo/sqlmodel/pull/715) by [@tiangolo](https://github.com/tiangolo). ### Internal From 276bcf788c17bc387984c228d4a5d22bb43062e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 4 Dec 2023 13:00:47 +0100 Subject: [PATCH 319/906] =?UTF-8?q?=F0=9F=94=A7=20Update=20docs=20build=20?= =?UTF-8?q?setup,=20add=20support=20for=20sponsors,=20add=20sponsor=20GOVC?= =?UTF-8?q?ERT.LU=20(#720)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 10 +- README.md | 10 +- data/sponsors.yml | 6 ++ docs/img/sponsors/govcert.png | Bin 0 -> 10063 bytes docs/index.md | 19 ++++ mkdocs.insiders.yml | 5 +- mkdocs.maybe-insiders.yml | 6 ++ mkdocs.no-insiders.yml | 0 mkdocs.yml | 28 +++--- pyproject.toml | 2 + scripts/docs.py | 155 +++++++++++++++++++++++++++++++ 11 files changed, 220 insertions(+), 21 deletions(-) create mode 100644 data/sponsors.yml create mode 100644 docs/img/sponsors/govcert.png create mode 100644 mkdocs.maybe-insiders.yml create mode 100644 mkdocs.no-insiders.yml create mode 100644 scripts/docs.py diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 3d29204f78..2e60ed7d39 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -30,6 +30,8 @@ jobs: - pyproject.toml - mkdocs.yml - mkdocs.insiders.yml + - ./github/workflows/build-docs.yml + - ./github/workflows/deploy-docs.yml build-docs: needs: @@ -69,12 +71,10 @@ jobs: with: key: mkdocs-cards-${{ github.ref }} path: .cache + - name: Verify README + run: python ./scripts/docs.py verify-readme - name: Build Docs - if: github.event_name == 'pull_request' && github.secret_source != 'Actions' - run: python -m poetry run mkdocs build - - name: Build Docs with Insiders - if: github.event_name != 'pull_request' || github.secret_source == 'Actions' - run: python -m poetry run mkdocs build --config-file mkdocs.insiders.yml + run: python ./scripts/docs.py build - uses: actions/upload-artifact@v3 with: name: docs-site diff --git a/README.md b/README.md index a9387c510a..ba3bb2196e 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,14 @@ The key features are: * **Extensible**: You have all the power of SQLAlchemy and Pydantic underneath. * **Short**: Minimize code duplication. A single type annotation does a lot of work. No need to duplicate models in SQLAlchemy and Pydantic. +## Sponsors + + + + + + + ## SQL Databases in FastAPI @@ -68,7 +76,7 @@ Successfully installed sqlmodel ## Example -For an introduction to databases, SQL, and everything else, see the SQLModel documentation. +For an introduction to databases, SQL, and everything else, see the SQLModel documentation. Here's a quick example. ✨ diff --git a/data/sponsors.yml b/data/sponsors.yml new file mode 100644 index 0000000000..95cf878530 --- /dev/null +++ b/data/sponsors.yml @@ -0,0 +1,6 @@ +gold: [] +silver: + - url: https://www.govcert.lu + title: This project is being supported by GOVCERT.LU + img: https://sqlmodel.tiangolo.com/img/sponsors/govcert.png +bronze: [] diff --git a/docs/img/sponsors/govcert.png b/docs/img/sponsors/govcert.png new file mode 100644 index 0000000000000000000000000000000000000000..0bb4cb934ae3a8f9aa3d1ce04546eda5b51be4fb GIT binary patch literal 10063 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 +.md-content .md-typeset h1 { display: none; } + +

SQLModel

@@ -38,6 +42,21 @@ The key features are: * **Extensible**: You have all the power of SQLAlchemy and Pydantic underneath. * **Short**: Minimize code duplication. A single type annotation does a lot of work. No need to duplicate models in SQLAlchemy and Pydantic. +## Sponsors + + + +{% if sponsors %} +{% for sponsor in sponsors.gold -%} + +{% endfor -%} +{%- for sponsor in sponsors.silver -%} + +{% endfor %} +{% endif %} + + + ## SQL Databases in FastAPI diff --git a/mkdocs.insiders.yml b/mkdocs.insiders.yml index 9f2775ff97..d24d754930 100644 --- a/mkdocs.insiders.yml +++ b/mkdocs.insiders.yml @@ -1,4 +1,3 @@ -INHERIT: mkdocs.yml plugins: - - search - - social + social: + typeset: diff --git a/mkdocs.maybe-insiders.yml b/mkdocs.maybe-insiders.yml new file mode 100644 index 0000000000..07aefaaa99 --- /dev/null +++ b/mkdocs.maybe-insiders.yml @@ -0,0 +1,6 @@ +# Define this here and not in the main mkdocs.yml file because that one could be auto +# updated and written, and the script would remove the env var +INHERIT: !ENV [INSIDERS_FILE, './mkdocs.no-insiders.yml'] +markdown_extensions: + pymdownx.highlight: + linenums: !ENV [LINENUMS, false] diff --git a/mkdocs.no-insiders.yml b/mkdocs.no-insiders.yml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/mkdocs.yml b/mkdocs.yml index a41839c12f..ce98f1524e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,3 +1,4 @@ +INHERIT: ./mkdocs.maybe-insiders.yml site_name: SQLModel site_description: SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness. site_url: https://sqlmodel.tiangolo.com/ @@ -36,6 +37,11 @@ theme: repo_name: tiangolo/sqlmodel repo_url: https://github.com/tiangolo/sqlmodel edit_uri: '' +plugins: + search: null + markdownextradata: + data: ./data + nav: - SQLModel: index.md - features.md @@ -98,30 +104,28 @@ nav: - release-notes.md markdown_extensions: -- markdown.extensions.attr_list -- markdown.extensions.tables -- markdown.extensions.md_in_html -- toc: + markdown.extensions.attr_list: + markdown.extensions.tables: + markdown.extensions.md_in_html: + toc: permalink: true -- pymdownx.superfences: + pymdownx.superfences: custom_fences: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.betterem -- pymdownx.highlight: - linenums: !ENV [LINENUMS, false] -- pymdownx.blocks.details -- pymdownx.blocks.admonition: + pymdownx.betterem: + pymdownx.blocks.details: + pymdownx.blocks.admonition: types: - note - info - tip - warning - danger -- pymdownx.blocks.tab: + pymdownx.blocks.tab: alternate_style: True -- mdx_include + mdx_include: extra: analytics: diff --git a/pyproject.toml b/pyproject.toml index 9bfc434cfb..24a6c5c22e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,8 @@ fastapi = "^0.103.2" ruff = "^0.1.2" # For FastAPI tests httpx = "0.24.1" +typer-cli = "^0.0.13" +mkdocs-markdownextradata-plugin = ">=0.1.7,<0.3.0" [build-system] requires = ["poetry-core"] diff --git a/scripts/docs.py b/scripts/docs.py new file mode 100644 index 0000000000..cab6c87db1 --- /dev/null +++ b/scripts/docs.py @@ -0,0 +1,155 @@ +import logging +import os +import re +import subprocess +from functools import lru_cache +from http.server import HTTPServer, SimpleHTTPRequestHandler +from importlib import metadata +from pathlib import Path + +import mkdocs.commands.build +import mkdocs.commands.serve +import mkdocs.config +import mkdocs.utils +import typer +from jinja2 import Template + +logging.basicConfig(level=logging.INFO) + +mkdocs_name = "mkdocs.yml" +en_docs_path = Path("") + +app = typer.Typer() + + +@lru_cache +def is_mkdocs_insiders() -> bool: + version = metadata.version("mkdocs-material") + return "insiders" in version + + +@app.callback() +def callback() -> None: + if is_mkdocs_insiders(): + os.environ["INSIDERS_FILE"] = "./mkdocs.insiders.yml" + # For MacOS with insiders and Cairo + os.environ["DYLD_FALLBACK_LIBRARY_PATH"] = "/opt/homebrew/lib" + + +index_sponsors_template = """ +{% if sponsors %} +{% for sponsor in sponsors.gold -%} + +{% endfor -%} +{%- for sponsor in sponsors.silver -%} + +{% endfor %} +{% endif %} +""" + + +def generate_readme_content() -> str: + en_index = en_docs_path / "docs" / "index.md" + content = en_index.read_text("utf-8") + match_pre = re.search(r"\n\n", content) + match_start = re.search(r"", content) + match_end = re.search(r"", content) + sponsors_data_path = en_docs_path / "data" / "sponsors.yml" + sponsors = mkdocs.utils.yaml_load(sponsors_data_path.read_text(encoding="utf-8")) + if not (match_start and match_end): + raise RuntimeError("Couldn't auto-generate sponsors section") + if not match_pre: + raise RuntimeError("Couldn't find pre section (
hero
hero
id
id
name
name
secret_name
secret_name
age
age
team_id
team_id
1
1
Deadpond
Deadpond
Dive Wilson
Dive Wilson
null
null
2
2
2
2
Spider-Boy
Spider-Boy
Pedro Parqueador
Pedro Parqueador
null
null
1
1
3
3
Rusty-Man
Rusty-Man
Tommy Sharp
Tommy Sharp
48
48
1
1
team
team
id
id
name
name
headquarters
headquarters
1
1
Preventers
Preventers
Sharp Tower
Sharp Tower
2
2
Z-Force
Z-Force

Sister Margaret’s Bar

Sister Margaret’s Bar
Viewer does not support full SVG 1.1 +
hero
hero
id
id
name
name
secret_name
secret_name
age
age
team_id
team_id
1
1
Deadpond
Deadpond
Dive Wilson
Dive Wilson
null
null
2
2
2
2
Spider-Boy
Spider-Boy
Pedro Parqueador
Pedro Parqueador
null
null
1
1
3
3
Rusty-Man
Rusty-Man
Tommy Sharp
Tommy Sharp
48
48
1
1
team
team
id
id
name
name
headquarters
headquarters
1
1
Preventers
Preventers
Sharp Tower
Sharp Tower
2
2
Z-Force
Z-Force

Sister Margaret's Bar

Sister Margaret's Bar
Viewer does not support full SVG 1.1 diff --git a/docs/img/tutorial/many-to-many/many-to-many.drawio b/docs/img/tutorial/many-to-many/many-to-many.drawio index bbe1176076..b33e547fc8 100644 --- a/docs/img/tutorial/many-to-many/many-to-many.drawio +++ b/docs/img/tutorial/many-to-many/many-to-many.drawio @@ -103,7 +103,7 @@ - + diff --git a/docs/img/tutorial/many-to-many/many-to-many.svg b/docs/img/tutorial/many-to-many/many-to-many.svg index aa0c6cc457..847a8351f0 100644 --- a/docs/img/tutorial/many-to-many/many-to-many.svg +++ b/docs/img/tutorial/many-to-many/many-to-many.svg @@ -54,4 +54,4 @@ src: url("https://fonts.gstatic.com/s/roboto/v27/KFOmCnqEu92Fr1Mu4mxK.woff2") format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } -
hero
hero
id
id
name
name
secret_name
secret_name
age
age
1
1
Deadpond
Deadpond
Dive Wilson
Dive Wilson
null
null
2
2
Spider-Boy
Spider-Boy
Pedro Parqueador
Pedro Parqueador
null
null
3
3
Rusty-Man
Rusty-Man
Tommy Sharp
Tommy Sharp
48
48
team
team
id
id
name
name
headquarters
headquarters
1
1
Preventers
Preventers
Sharp Tower
Sharp Tower
2
2
Z-Force
Z-Force

Sister Margaret’s Bar

Sister Margaret’s Bar
heroteamlink
heroteamlink
hero_id
hero_id
team_id
team_id
1
1
1
1
1
1
2
2
2
2
1
1
3
3
1
1
Viewer does not support full SVG 1.1 +
hero
hero
id
id
name
name
secret_name
secret_name
age
age
1
1
Deadpond
Deadpond
Dive Wilson
Dive Wilson
null
null
2
2
Spider-Boy
Spider-Boy
Pedro Parqueador
Pedro Parqueador
null
null
3
3
Rusty-Man
Rusty-Man
Tommy Sharp
Tommy Sharp
48
48
team
team
id
id
name
name
headquarters
headquarters
1
1
Preventers
Preventers
Sharp Tower
Sharp Tower
2
2
Z-Force
Z-Force

Sister Margaret's Bar

Sister Margaret's Bar
heroteamlink
heroteamlink
hero_id
hero_id
team_id
team_id
1
1
1
1
1
1
2
2
2
2
1
1
3
3
1
1
Viewer does not support full SVG 1.1 diff --git a/docs/img/tutorial/relationships/select/relationships2.drawio b/docs/img/tutorial/relationships/select/relationships2.drawio index afd77f4e9e..e3f25a203e 100644 --- a/docs/img/tutorial/relationships/select/relationships2.drawio +++ b/docs/img/tutorial/relationships/select/relationships2.drawio @@ -115,7 +115,7 @@ - + diff --git a/docs/img/tutorial/relationships/select/relationships2.svg b/docs/img/tutorial/relationships/select/relationships2.svg index c0987c7c32..ed293b2350 100644 --- a/docs/img/tutorial/relationships/select/relationships2.svg +++ b/docs/img/tutorial/relationships/select/relationships2.svg @@ -54,4 +54,4 @@ src: url("https://fonts.gstatic.com/s/roboto/v27/KFOmCnqEu92Fr1Mu4mxK.woff2") format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } -
hero
hero
id
id
name
name
secret_name
secret_name
age
age
team_id
team_id
1
1
Deadpond
Deadpond
Dive Wilson
Dive Wilson
null
null
2
2
2
2
Spider-Boy
Spider-Boy
Pedro Parqueador
Pedro Parqueador
null
null
null
null
3
3
Rusty-Man
Rusty-Man
Tommy Sharp
Tommy Sharp
48
48
1
1
team
team
id
id
name
name
headquarters
headquarters
1
1
Preventers
Preventers
Sharp Tower
Sharp Tower
2
2
Z-Force
Z-Force

Sister Margaret’s Bar

Sister Margaret’s Bar
Viewer does not support full SVG 1.1 +
hero
hero
id
id
name
name
secret_name
secret_name
age
age
team_id
team_id
1
1
Deadpond
Deadpond
Dive Wilson
Dive Wilson
null
null
2
2
2
2
Spider-Boy
Spider-Boy
Pedro Parqueador
Pedro Parqueador
null
null
null
null
3
3
Rusty-Man
Rusty-Man
Tommy Sharp
Tommy Sharp
48
48
1
1
team
team
id
id
name
name
headquarters
headquarters
1
1
Preventers
Preventers
Sharp Tower
Sharp Tower
2
2
Z-Force
Z-Force

Sister Margaret's Bar

Sister Margaret's Bar
Viewer does not support full SVG 1.1 diff --git a/docs/tutorial/code-structure.md b/docs/tutorial/code-structure.md index 0152ff669e..386b5c8e4d 100644 --- a/docs/tutorial/code-structure.md +++ b/docs/tutorial/code-structure.md @@ -138,7 +138,7 @@ So, the output would be: $ python -m project.app Created hero: id=1 secret_name='Dive Wilson' team_id=1 name='Deadpond' age=None -Hero's team: name='Z-Force' headquarters='Sister Margaret’s Bar' id=1 +Hero's team: name='Z-Force' headquarters='Sister Margaret's Bar' id=1 ``` @@ -243,7 +243,7 @@ And running that achieves the same result as before: $ python -m project.app Created hero: id=1 age=None name='Deadpond' secret_name='Dive Wilson' team_id=1 -Hero's team: id=1 name='Z-Force' headquarters='Sister Margaret’s Bar' +Hero's team: id=1 name='Z-Force' headquarters='Sister Margaret's Bar' ``` diff --git a/docs/tutorial/connect/create-connected-rows.md b/docs/tutorial/connect/create-connected-rows.md index f8845ac772..e2818c95e8 100644 --- a/docs/tutorial/connect/create-connected-rows.md +++ b/docs/tutorial/connect/create-connected-rows.md @@ -12,7 +12,7 @@ The `team` table will look like this: 1PreventersSharp Tower -2Z-ForceSister Margaret’s Bar +2Z-ForceSister Margaret's Bar @@ -126,7 +126,7 @@ INFO Engine BEGIN (implicit) INFO Engine INSERT INTO team (name, headquarters) VALUES (?, ?) INFO Engine [generated in 0.00050s] ('Preventers', 'Sharp Tower') INFO Engine INSERT INTO team (name, headquarters) VALUES (?, ?) -INFO Engine [cached since 0.002324s ago] ('Z-Force', 'Sister Margaret’s Bar') +INFO Engine [cached since 0.002324s ago] ('Z-Force', 'Sister Margaret's Bar') INFO Engine COMMIT ``` diff --git a/docs/tutorial/connect/create-connected-tables.md b/docs/tutorial/connect/create-connected-tables.md index 411ab3271e..a27cbfed7a 100644 --- a/docs/tutorial/connect/create-connected-tables.md +++ b/docs/tutorial/connect/create-connected-tables.md @@ -16,7 +16,7 @@ The team table will look like this: 1PreventersSharp Tower -2Z-ForceSister Margaret’s Bar +2Z-ForceSister Margaret's Bar diff --git a/docs/tutorial/connect/read-connected-data.md b/docs/tutorial/connect/read-connected-data.md index 991d4409c8..128eb550b7 100644 --- a/docs/tutorial/connect/read-connected-data.md +++ b/docs/tutorial/connect/read-connected-data.md @@ -12,7 +12,7 @@ The `team` table has this data: 1PreventersSharp Tower -2Z-ForceSister Margaret’s Bar +2Z-ForceSister Margaret's Bar @@ -214,7 +214,7 @@ WHERE hero.team_id = team.id 2021-08-09 08:55:50,682 INFO sqlalchemy.engine.Engine [no key 0.00015s] () // Print the first hero and team -Hero: id=1 secret_name='Dive Wilson' team_id=2 name='Deadpond' age=None Team: headquarters='Sister Margaret’s Bar' id=2 name='Z-Force' +Hero: id=1 secret_name='Dive Wilson' team_id=2 name='Deadpond' age=None Team: headquarters='Sister Margaret's Bar' id=2 name='Z-Force' // Print the second hero and team Hero: id=2 secret_name='Tommy Sharp' team_id=1 name='Rusty-Man' age=48 Team: headquarters='Sharp Tower' id=1 name='Preventers' @@ -335,7 +335,7 @@ FROM hero JOIN team ON team.id = hero.team_id INFO Engine [no key 0.00032s] () // Print the first hero and team -Hero: id=1 secret_name='Dive Wilson' team_id=2 name='Deadpond' age=None Team: headquarters='Sister Margaret’s Bar' id=2 name='Z-Force' +Hero: id=1 secret_name='Dive Wilson' team_id=2 name='Deadpond' age=None Team: headquarters='Sister Margaret's Bar' id=2 name='Z-Force' // Print the second hero and team Hero: id=2 secret_name='Tommy Sharp' team_id=1 name='Rusty-Man' age=48 Team: headquarters='Sharp Tower' id=1 name='Preventers' @@ -473,7 +473,7 @@ FROM hero LEFT OUTER JOIN team ON team.id = hero.team_id INFO Engine [no key 0.00051s] () // Print the first hero and team -Hero: id=1 secret_name='Dive Wilson' team_id=2 name='Deadpond' age=None Team: headquarters='Sister Margaret’s Bar' id=2 name='Z-Force' +Hero: id=1 secret_name='Dive Wilson' team_id=2 name='Deadpond' age=None Team: headquarters='Sister Margaret's Bar' id=2 name='Z-Force' // Print the second hero and team Hero: id=2 secret_name='Tommy Sharp' team_id=1 name='Rusty-Man' age=48 Team: headquarters='Sharp Tower' id=1 name='Preventers' // Print the third hero and team, we included Spider-Boy 🎉 diff --git a/docs/tutorial/connect/remove-data-connections.md b/docs/tutorial/connect/remove-data-connections.md index b220530043..72e933cb6f 100644 --- a/docs/tutorial/connect/remove-data-connections.md +++ b/docs/tutorial/connect/remove-data-connections.md @@ -10,7 +10,7 @@ We currently have a `team` table: 1PreventersSharp Tower -2Z-ForceSister Margaret’s Bar +2Z-ForceSister Margaret's Bar diff --git a/docs/tutorial/connect/update-data-connections.md b/docs/tutorial/connect/update-data-connections.md index 39994c3e91..c141aa69d4 100644 --- a/docs/tutorial/connect/update-data-connections.md +++ b/docs/tutorial/connect/update-data-connections.md @@ -10,7 +10,7 @@ At this point we have a `team` table: 1PreventersSharp Tower -2Z-ForceSister Margaret’s Bar +2Z-ForceSister Margaret's Bar diff --git a/docs/tutorial/fastapi/relationships.md b/docs/tutorial/fastapi/relationships.md index 81cf4286be..3275dcfa17 100644 --- a/docs/tutorial/fastapi/relationships.md +++ b/docs/tutorial/fastapi/relationships.md @@ -263,7 +263,7 @@ Now we get the **team** data included: "id": 1, "team": { "name": "Z-Force", - "headquarters": "Sister Margaret’s Bar", + "headquarters": "Sister Margaret's Bar", "id": 1 } } diff --git a/docs/tutorial/many-to-many/create-data.md b/docs/tutorial/many-to-many/create-data.md index 3a7719c960..1925316a09 100644 --- a/docs/tutorial/many-to-many/create-data.md +++ b/docs/tutorial/many-to-many/create-data.md @@ -106,7 +106,7 @@ INFO Engine INSERT INTO hero (name, secret_name, age) VALUES (?, ?, ?) INFO Engine [cached since 0.002541s ago] ('Spider-Boy', 'Pedro Parqueador', None) // Insert the team data second INFO Engine INSERT INTO team (name, headquarters) VALUES (?, ?) -INFO Engine [generated in 0.00037s] ('Z-Force', 'Sister Margaret’s Bar') +INFO Engine [generated in 0.00037s] ('Z-Force', 'Sister Margaret's Bar') INFO Engine INSERT INTO team (name, headquarters) VALUES (?, ?) INFO Engine [cached since 0.001239s ago] ('Preventers', 'Sharp Tower') // Insert the link data last, to be able to re-use the created IDs @@ -141,7 +141,7 @@ WHERE ? = heroteamlink.hero_id AND team.id = heroteamlink.team_id INFO Engine [generated in 0.00025s] (1,) // Print Deadpond's teams, 2 teams! 🎉 -Deadpond teams: [Team(id=1, name='Z-Force', headquarters='Sister Margaret’s Bar'), Team(id=2, name='Preventers', headquarters='Sharp Tower')] +Deadpond teams: [Team(id=1, name='Z-Force', headquarters='Sister Margaret's Bar'), Team(id=2, name='Preventers', headquarters='Sharp Tower')] // Print Rusty-Man Rusty-Man: name='Rusty-Man' age=48 id=2 secret_name='Tommy Sharp' diff --git a/docs/tutorial/many-to-many/index.md b/docs/tutorial/many-to-many/index.md index 2aa2cf52f9..ebbefbe1ee 100644 --- a/docs/tutorial/many-to-many/index.md +++ b/docs/tutorial/many-to-many/index.md @@ -26,7 +26,7 @@ The `team` table looks like this: 1PreventersSharp Tower -2Z-ForceSister Margaret’s Bar +2Z-ForceSister Margaret's Bar diff --git a/docs/tutorial/many-to-many/link-with-extra-fields.md b/docs/tutorial/many-to-many/link-with-extra-fields.md index bda78d1ec3..b7e06c2b4e 100644 --- a/docs/tutorial/many-to-many/link-with-extra-fields.md +++ b/docs/tutorial/many-to-many/link-with-extra-fields.md @@ -153,7 +153,7 @@ INFO Engine [cached since 0.001858s ago] ('Rusty-Man', 'Tommy Sharp', 48) // Insert the teams INFO Engine INSERT INTO team (name, headquarters) VALUES (?, ?) -INFO Engine [generated in 0.00019s] ('Z-Force', 'Sister Margaret’s Bar') +INFO Engine [generated in 0.00019s] ('Z-Force', 'Sister Margaret's Bar') INFO Engine INSERT INTO team (name, headquarters) VALUES (?, ?) INFO Engine [cached since 0.0007985s ago] ('Preventers', 'Sharp Tower') @@ -389,7 +389,7 @@ WHERE team.id = ? INFO Engine [cached since 0.2097s ago] (1,) // Print Spider-Boy team, including link data, if is training -Spider-Boy team: headquarters='Sister Margaret’s Bar' id=1 name='Z-Force' is training: True +Spider-Boy team: headquarters='Sister Margaret's Bar' id=1 name='Z-Force' is training: True INFO Engine ROLLBACK ``` diff --git a/docs/tutorial/many-to-many/update-remove-relationships.md b/docs/tutorial/many-to-many/update-remove-relationships.md index ff4f5dd096..0e83e24d2d 100644 --- a/docs/tutorial/many-to-many/update-remove-relationships.md +++ b/docs/tutorial/many-to-many/update-remove-relationships.md @@ -126,7 +126,7 @@ INFO Engine [cached since 0.1648s ago] (3,) // Print Spider-Boy teams, including Z-Force 🎉 Updated Spider-Boy's Teams: [ Team(id=2, name='Preventers', headquarters='Sharp Tower'), - Team(id=1, name='Z-Force', headquarters='Sister Margaret’s Bar') + Team(id=1, name='Z-Force', headquarters='Sister Margaret's Bar') ] // Automatically refresh the data while accessing the attribute .heores @@ -140,7 +140,7 @@ Z-Force heroes: [ Hero(name='Deadpond', age=None, id=1, secret_name='Dive Wilson'), Hero(name='Spider-Boy', age=None, id=3, secret_name='Pedro Parqueador', teams=[ Team(id=2, name='Preventers', headquarters='Sharp Tower'), - Team(id=1, name='Z-Force', headquarters='Sister Margaret’s Bar', heroes=[...]) + Team(id=1, name='Z-Force', headquarters='Sister Margaret's Bar', heroes=[...]) ]) ] ``` diff --git a/docs/tutorial/relationship-attributes/define-relationships-attributes.md b/docs/tutorial/relationship-attributes/define-relationships-attributes.md index 7585480923..a5363ab027 100644 --- a/docs/tutorial/relationship-attributes/define-relationships-attributes.md +++ b/docs/tutorial/relationship-attributes/define-relationships-attributes.md @@ -14,7 +14,7 @@ We currently have a `team` table: 1PreventersSharp Tower -2Z-ForceSister Margaret’s Bar +2Z-ForceSister Margaret's Bar diff --git a/docs_src/tutorial/code_structure/tutorial001/app.py b/docs_src/tutorial/code_structure/tutorial001/app.py index 065f8a78b5..3d1bfc69a0 100644 --- a/docs_src/tutorial/code_structure/tutorial001/app.py +++ b/docs_src/tutorial/code_structure/tutorial001/app.py @@ -6,7 +6,7 @@ def create_heroes(): with Session(engine) as session: - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", secret_name="Dive Wilson", team=team_z_force diff --git a/docs_src/tutorial/code_structure/tutorial001_py310/app.py b/docs_src/tutorial/code_structure/tutorial001_py310/app.py index 065f8a78b5..3d1bfc69a0 100644 --- a/docs_src/tutorial/code_structure/tutorial001_py310/app.py +++ b/docs_src/tutorial/code_structure/tutorial001_py310/app.py @@ -6,7 +6,7 @@ def create_heroes(): with Session(engine) as session: - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", secret_name="Dive Wilson", team=team_z_force diff --git a/docs_src/tutorial/code_structure/tutorial001_py39/app.py b/docs_src/tutorial/code_structure/tutorial001_py39/app.py index 065f8a78b5..3d1bfc69a0 100644 --- a/docs_src/tutorial/code_structure/tutorial001_py39/app.py +++ b/docs_src/tutorial/code_structure/tutorial001_py39/app.py @@ -6,7 +6,7 @@ def create_heroes(): with Session(engine) as session: - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", secret_name="Dive Wilson", team=team_z_force diff --git a/docs_src/tutorial/code_structure/tutorial002/app.py b/docs_src/tutorial/code_structure/tutorial002/app.py index 8afaee7c16..2ecaec0c3c 100644 --- a/docs_src/tutorial/code_structure/tutorial002/app.py +++ b/docs_src/tutorial/code_structure/tutorial002/app.py @@ -7,7 +7,7 @@ def create_heroes(): with Session(engine) as session: - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", secret_name="Dive Wilson", team=team_z_force diff --git a/docs_src/tutorial/code_structure/tutorial002_py310/app.py b/docs_src/tutorial/code_structure/tutorial002_py310/app.py index 8afaee7c16..2ecaec0c3c 100644 --- a/docs_src/tutorial/code_structure/tutorial002_py310/app.py +++ b/docs_src/tutorial/code_structure/tutorial002_py310/app.py @@ -7,7 +7,7 @@ def create_heroes(): with Session(engine) as session: - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", secret_name="Dive Wilson", team=team_z_force diff --git a/docs_src/tutorial/code_structure/tutorial002_py39/app.py b/docs_src/tutorial/code_structure/tutorial002_py39/app.py index 8afaee7c16..2ecaec0c3c 100644 --- a/docs_src/tutorial/code_structure/tutorial002_py39/app.py +++ b/docs_src/tutorial/code_structure/tutorial002_py39/app.py @@ -7,7 +7,7 @@ def create_heroes(): with Session(engine) as session: - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", secret_name="Dive Wilson", team=team_z_force diff --git a/docs_src/tutorial/connect/delete/tutorial001.py b/docs_src/tutorial/connect/delete/tutorial001.py index eeb376a0cc..aa7d0db287 100644 --- a/docs_src/tutorial/connect/delete/tutorial001.py +++ b/docs_src/tutorial/connect/delete/tutorial001.py @@ -31,7 +31,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") session.add(team_preventers) session.add(team_z_force) session.commit() diff --git a/docs_src/tutorial/connect/delete/tutorial001_py310.py b/docs_src/tutorial/connect/delete/tutorial001_py310.py index 4815ad4e34..de0dd8d7e7 100644 --- a/docs_src/tutorial/connect/delete/tutorial001_py310.py +++ b/docs_src/tutorial/connect/delete/tutorial001_py310.py @@ -29,7 +29,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") session.add(team_preventers) session.add(team_z_force) session.commit() diff --git a/docs_src/tutorial/connect/insert/tutorial001.py b/docs_src/tutorial/connect/insert/tutorial001.py index dc3661d7c7..d2e3b2f0e3 100644 --- a/docs_src/tutorial/connect/insert/tutorial001.py +++ b/docs_src/tutorial/connect/insert/tutorial001.py @@ -31,7 +31,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") session.add(team_preventers) session.add(team_z_force) session.commit() diff --git a/docs_src/tutorial/connect/insert/tutorial001_py310.py b/docs_src/tutorial/connect/insert/tutorial001_py310.py index 506429dc1a..16c2c7fe09 100644 --- a/docs_src/tutorial/connect/insert/tutorial001_py310.py +++ b/docs_src/tutorial/connect/insert/tutorial001_py310.py @@ -29,7 +29,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") session.add(team_preventers) session.add(team_z_force) session.commit() diff --git a/docs_src/tutorial/connect/select/tutorial001.py b/docs_src/tutorial/connect/select/tutorial001.py index d4cdf413f1..d98e635779 100644 --- a/docs_src/tutorial/connect/select/tutorial001.py +++ b/docs_src/tutorial/connect/select/tutorial001.py @@ -31,7 +31,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") session.add(team_preventers) session.add(team_z_force) session.commit() diff --git a/docs_src/tutorial/connect/select/tutorial001_py310.py b/docs_src/tutorial/connect/select/tutorial001_py310.py index c39894fce9..faa25b3891 100644 --- a/docs_src/tutorial/connect/select/tutorial001_py310.py +++ b/docs_src/tutorial/connect/select/tutorial001_py310.py @@ -29,7 +29,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") session.add(team_preventers) session.add(team_z_force) session.commit() diff --git a/docs_src/tutorial/connect/select/tutorial002.py b/docs_src/tutorial/connect/select/tutorial002.py index 59edbf7fd9..270f95003d 100644 --- a/docs_src/tutorial/connect/select/tutorial002.py +++ b/docs_src/tutorial/connect/select/tutorial002.py @@ -31,7 +31,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") session.add(team_preventers) session.add(team_z_force) session.commit() diff --git a/docs_src/tutorial/connect/select/tutorial002_py310.py b/docs_src/tutorial/connect/select/tutorial002_py310.py index 756dab098b..08adc69d32 100644 --- a/docs_src/tutorial/connect/select/tutorial002_py310.py +++ b/docs_src/tutorial/connect/select/tutorial002_py310.py @@ -29,7 +29,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") session.add(team_preventers) session.add(team_z_force) session.commit() diff --git a/docs_src/tutorial/connect/select/tutorial003.py b/docs_src/tutorial/connect/select/tutorial003.py index fb5b8aa0c9..ee427e309c 100644 --- a/docs_src/tutorial/connect/select/tutorial003.py +++ b/docs_src/tutorial/connect/select/tutorial003.py @@ -31,7 +31,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") session.add(team_preventers) session.add(team_z_force) session.commit() diff --git a/docs_src/tutorial/connect/select/tutorial003_py310.py b/docs_src/tutorial/connect/select/tutorial003_py310.py index 63f5b27350..07127c6901 100644 --- a/docs_src/tutorial/connect/select/tutorial003_py310.py +++ b/docs_src/tutorial/connect/select/tutorial003_py310.py @@ -29,7 +29,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") session.add(team_preventers) session.add(team_z_force) session.commit() diff --git a/docs_src/tutorial/connect/select/tutorial004.py b/docs_src/tutorial/connect/select/tutorial004.py index d1d260b3f4..29a7c205bd 100644 --- a/docs_src/tutorial/connect/select/tutorial004.py +++ b/docs_src/tutorial/connect/select/tutorial004.py @@ -31,7 +31,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") session.add(team_preventers) session.add(team_z_force) session.commit() diff --git a/docs_src/tutorial/connect/select/tutorial004_py310.py b/docs_src/tutorial/connect/select/tutorial004_py310.py index 8b024321d2..6bcdee96be 100644 --- a/docs_src/tutorial/connect/select/tutorial004_py310.py +++ b/docs_src/tutorial/connect/select/tutorial004_py310.py @@ -29,7 +29,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") session.add(team_preventers) session.add(team_z_force) session.commit() diff --git a/docs_src/tutorial/connect/select/tutorial005.py b/docs_src/tutorial/connect/select/tutorial005.py index a61ef8a015..96a12ab537 100644 --- a/docs_src/tutorial/connect/select/tutorial005.py +++ b/docs_src/tutorial/connect/select/tutorial005.py @@ -31,7 +31,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") session.add(team_preventers) session.add(team_z_force) session.commit() diff --git a/docs_src/tutorial/connect/select/tutorial005_py310.py b/docs_src/tutorial/connect/select/tutorial005_py310.py index 2120640bc4..445572a147 100644 --- a/docs_src/tutorial/connect/select/tutorial005_py310.py +++ b/docs_src/tutorial/connect/select/tutorial005_py310.py @@ -29,7 +29,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") session.add(team_preventers) session.add(team_z_force) session.commit() diff --git a/docs_src/tutorial/connect/update/tutorial001.py b/docs_src/tutorial/connect/update/tutorial001.py index 0080340532..b32599fc0c 100644 --- a/docs_src/tutorial/connect/update/tutorial001.py +++ b/docs_src/tutorial/connect/update/tutorial001.py @@ -31,7 +31,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") session.add(team_preventers) session.add(team_z_force) session.commit() diff --git a/docs_src/tutorial/connect/update/tutorial001_py310.py b/docs_src/tutorial/connect/update/tutorial001_py310.py index 1fbb108ba2..a6ebfa6ee0 100644 --- a/docs_src/tutorial/connect/update/tutorial001_py310.py +++ b/docs_src/tutorial/connect/update/tutorial001_py310.py @@ -29,7 +29,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") session.add(team_preventers) session.add(team_z_force) session.commit() diff --git a/docs_src/tutorial/many_to_many/tutorial001.py b/docs_src/tutorial/many_to_many/tutorial001.py index bb4e9d0896..79f9e7909a 100644 --- a/docs_src/tutorial/many_to_many/tutorial001.py +++ b/docs_src/tutorial/many_to_many/tutorial001.py @@ -42,7 +42,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", diff --git a/docs_src/tutorial/many_to_many/tutorial001_py310.py b/docs_src/tutorial/many_to_many/tutorial001_py310.py index 5e8f31caa0..e47d3920eb 100644 --- a/docs_src/tutorial/many_to_many/tutorial001_py310.py +++ b/docs_src/tutorial/many_to_many/tutorial001_py310.py @@ -36,7 +36,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", diff --git a/docs_src/tutorial/many_to_many/tutorial001_py39.py b/docs_src/tutorial/many_to_many/tutorial001_py39.py index 0d7325a723..c39fe91452 100644 --- a/docs_src/tutorial/many_to_many/tutorial001_py39.py +++ b/docs_src/tutorial/many_to_many/tutorial001_py39.py @@ -42,7 +42,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", diff --git a/docs_src/tutorial/many_to_many/tutorial002.py b/docs_src/tutorial/many_to_many/tutorial002.py index dc4aa0b770..9845340ec4 100644 --- a/docs_src/tutorial/many_to_many/tutorial002.py +++ b/docs_src/tutorial/many_to_many/tutorial002.py @@ -42,7 +42,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", diff --git a/docs_src/tutorial/many_to_many/tutorial002_py310.py b/docs_src/tutorial/many_to_many/tutorial002_py310.py index 5823a6e72e..2668161e92 100644 --- a/docs_src/tutorial/many_to_many/tutorial002_py310.py +++ b/docs_src/tutorial/many_to_many/tutorial002_py310.py @@ -36,7 +36,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", diff --git a/docs_src/tutorial/many_to_many/tutorial002_py39.py b/docs_src/tutorial/many_to_many/tutorial002_py39.py index 54c5d45356..c3b5f88f0e 100644 --- a/docs_src/tutorial/many_to_many/tutorial002_py39.py +++ b/docs_src/tutorial/many_to_many/tutorial002_py39.py @@ -42,7 +42,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", diff --git a/docs_src/tutorial/many_to_many/tutorial003.py b/docs_src/tutorial/many_to_many/tutorial003.py index 1e03c4af89..6c23878b01 100644 --- a/docs_src/tutorial/many_to_many/tutorial003.py +++ b/docs_src/tutorial/many_to_many/tutorial003.py @@ -46,7 +46,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", diff --git a/docs_src/tutorial/many_to_many/tutorial003_py310.py b/docs_src/tutorial/many_to_many/tutorial003_py310.py index b8fa4632fe..4c1ad12259 100644 --- a/docs_src/tutorial/many_to_many/tutorial003_py310.py +++ b/docs_src/tutorial/many_to_many/tutorial003_py310.py @@ -40,7 +40,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", diff --git a/docs_src/tutorial/many_to_many/tutorial003_py39.py b/docs_src/tutorial/many_to_many/tutorial003_py39.py index 214228a122..175fbf318b 100644 --- a/docs_src/tutorial/many_to_many/tutorial003_py39.py +++ b/docs_src/tutorial/many_to_many/tutorial003_py39.py @@ -46,7 +46,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", diff --git a/docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py b/docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py index fc4eb97934..39230cb697 100644 --- a/docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py +++ b/docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py @@ -34,7 +34,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", secret_name="Dive Wilson", team=team_z_force diff --git a/docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py b/docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py index fb35500af8..a6ee1cafba 100644 --- a/docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py +++ b/docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py @@ -32,7 +32,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", secret_name="Dive Wilson", team=team_z_force diff --git a/docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py b/docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py index a5deae91e9..6ec5e72a7c 100644 --- a/docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py +++ b/docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py @@ -34,7 +34,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", secret_name="Dive Wilson", team=team_z_force diff --git a/docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py b/docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py index a25df4e75d..a1add4bccb 100644 --- a/docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py +++ b/docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py @@ -34,7 +34,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", secret_name="Dive Wilson", team=team_z_force diff --git a/docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py b/docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py index 2113fde179..978ed99787 100644 --- a/docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py +++ b/docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py @@ -32,7 +32,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", secret_name="Dive Wilson", team=team_z_force diff --git a/docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py39.py b/docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py39.py index 72a8616958..d001ef03da 100644 --- a/docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py39.py +++ b/docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py39.py @@ -34,7 +34,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", secret_name="Dive Wilson", team=team_z_force diff --git a/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py b/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py index ec9c909d73..73c68cf965 100644 --- a/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py +++ b/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py @@ -34,7 +34,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", secret_name="Dive Wilson", team=team_z_force diff --git a/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py b/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py index 4e0314763e..35567360e1 100644 --- a/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py +++ b/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py @@ -32,7 +32,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", secret_name="Dive Wilson", team=team_z_force diff --git a/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py39.py b/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py39.py index bc51058a5a..43c699d3d9 100644 --- a/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py39.py +++ b/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py39.py @@ -34,7 +34,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", secret_name="Dive Wilson", team=team_z_force diff --git a/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py b/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py index 71cb3f6136..af218b9567 100644 --- a/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py +++ b/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py @@ -34,7 +34,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", secret_name="Dive Wilson", team=team_z_force diff --git a/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py b/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py index dc4e261bb0..5df80e9f86 100644 --- a/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py +++ b/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py @@ -32,7 +32,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", secret_name="Dive Wilson", team=team_z_force diff --git a/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py39.py b/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py39.py index 42ba84da2a..8530f67b39 100644 --- a/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py39.py +++ b/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py39.py @@ -34,7 +34,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", secret_name="Dive Wilson", team=team_z_force diff --git a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py index 3b130072b7..b8bbe70a4c 100644 --- a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py +++ b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py @@ -34,7 +34,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", secret_name="Dive Wilson", team=team_z_force diff --git a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py310.py b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py310.py index 18b3f178ca..abae89d2eb 100644 --- a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py310.py +++ b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py310.py @@ -32,7 +32,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", secret_name="Dive Wilson", team=team_z_force diff --git a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py39.py b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py39.py index 18c4c2edf1..2219c82dab 100644 --- a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py39.py +++ b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py39.py @@ -34,7 +34,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", secret_name="Dive Wilson", team=team_z_force diff --git a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py index fdb436eb5f..30fa840878 100644 --- a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py +++ b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py @@ -34,7 +34,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", secret_name="Dive Wilson", team=team_z_force diff --git a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py310.py b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py310.py index f1320024ec..0221d8834c 100644 --- a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py310.py +++ b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py310.py @@ -32,7 +32,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", secret_name="Dive Wilson", team=team_z_force diff --git a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py39.py b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py39.py index 33310e7cde..393bdc8a5e 100644 --- a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py39.py +++ b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py39.py @@ -34,7 +34,7 @@ def create_db_and_tables(): def create_heroes(): with Session(engine) as session: team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") hero_deadpond = Hero( name="Deadpond", secret_name="Dive Wilson", team=team_z_force diff --git a/tests/test_tutorial/test_code_structure/test_tutorial001.py b/tests/test_tutorial/test_code_structure/test_tutorial001.py index 4192e00434..c6e3158360 100644 --- a/tests/test_tutorial/test_code_structure/test_tutorial001.py +++ b/tests/test_tutorial/test_code_structure/test_tutorial001.py @@ -17,7 +17,7 @@ ], [ "Hero's team:", - {"name": "Z-Force", "headquarters": "Sister Margaret’s Bar", "id": 1}, + {"name": "Z-Force", "headquarters": "Sister Margaret's Bar", "id": 1}, ], ] diff --git a/tests/test_tutorial/test_code_structure/test_tutorial001_py310.py b/tests/test_tutorial/test_code_structure/test_tutorial001_py310.py index 31965600e6..44d9d920fa 100644 --- a/tests/test_tutorial/test_code_structure/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_code_structure/test_tutorial001_py310.py @@ -17,7 +17,7 @@ ], [ "Hero's team:", - {"name": "Z-Force", "headquarters": "Sister Margaret’s Bar", "id": 1}, + {"name": "Z-Force", "headquarters": "Sister Margaret's Bar", "id": 1}, ], ] diff --git a/tests/test_tutorial/test_code_structure/test_tutorial001_py39.py b/tests/test_tutorial/test_code_structure/test_tutorial001_py39.py index b101dea055..b17917cff2 100644 --- a/tests/test_tutorial/test_code_structure/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_code_structure/test_tutorial001_py39.py @@ -17,7 +17,7 @@ ], [ "Hero's team:", - {"name": "Z-Force", "headquarters": "Sister Margaret’s Bar", "id": 1}, + {"name": "Z-Force", "headquarters": "Sister Margaret's Bar", "id": 1}, ], ] diff --git a/tests/test_tutorial/test_code_structure/test_tutorial002.py b/tests/test_tutorial/test_code_structure/test_tutorial002.py index 37e4e54de6..8e7ac8f173 100644 --- a/tests/test_tutorial/test_code_structure/test_tutorial002.py +++ b/tests/test_tutorial/test_code_structure/test_tutorial002.py @@ -17,7 +17,7 @@ ], [ "Hero's team:", - {"name": "Z-Force", "headquarters": "Sister Margaret’s Bar", "id": 1}, + {"name": "Z-Force", "headquarters": "Sister Margaret's Bar", "id": 1}, ], ] diff --git a/tests/test_tutorial/test_code_structure/test_tutorial002_py310.py b/tests/test_tutorial/test_code_structure/test_tutorial002_py310.py index 5744aa9e35..3eafdee831 100644 --- a/tests/test_tutorial/test_code_structure/test_tutorial002_py310.py +++ b/tests/test_tutorial/test_code_structure/test_tutorial002_py310.py @@ -17,7 +17,7 @@ ], [ "Hero's team:", - {"name": "Z-Force", "headquarters": "Sister Margaret’s Bar", "id": 1}, + {"name": "Z-Force", "headquarters": "Sister Margaret's Bar", "id": 1}, ], ] diff --git a/tests/test_tutorial/test_code_structure/test_tutorial002_py39.py b/tests/test_tutorial/test_code_structure/test_tutorial002_py39.py index bae15c37e7..9b5eb670c2 100644 --- a/tests/test_tutorial/test_code_structure/test_tutorial002_py39.py +++ b/tests/test_tutorial/test_code_structure/test_tutorial002_py39.py @@ -17,7 +17,7 @@ ], [ "Hero's team:", - {"name": "Z-Force", "headquarters": "Sister Margaret’s Bar", "id": 1}, + {"name": "Z-Force", "headquarters": "Sister Margaret's Bar", "id": 1}, ], ] diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial001_py310_tutorial002_py310.py b/tests/test_tutorial/test_connect/test_select/test_tutorial001_py310_tutorial002_py310.py index 01762f3e7e..d3bab7f669 100644 --- a/tests/test_tutorial/test_connect/test_select/test_tutorial001_py310_tutorial002_py310.py +++ b/tests/test_tutorial/test_connect/test_select/test_tutorial001_py310_tutorial002_py310.py @@ -45,7 +45,7 @@ "name": "Deadpond", }, "Team:", - {"id": 2, "name": "Z-Force", "headquarters": "Sister Margaret’s Bar"}, + {"id": 2, "name": "Z-Force", "headquarters": "Sister Margaret's Bar"}, ], [ "Hero:", diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_connect/test_select/test_tutorial001_tutorial002.py index 32b29482d0..541a8ee00f 100644 --- a/tests/test_tutorial/test_connect/test_select/test_tutorial001_tutorial002.py +++ b/tests/test_tutorial/test_connect/test_select/test_tutorial001_tutorial002.py @@ -45,7 +45,7 @@ "name": "Deadpond", }, "Team:", - {"id": 2, "name": "Z-Force", "headquarters": "Sister Margaret’s Bar"}, + {"id": 2, "name": "Z-Force", "headquarters": "Sister Margaret's Bar"}, ], [ "Hero:", diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial003.py b/tests/test_tutorial/test_connect/test_select/test_tutorial003.py index ad6d4ccb86..2eab135add 100644 --- a/tests/test_tutorial/test_connect/test_select/test_tutorial003.py +++ b/tests/test_tutorial/test_connect/test_select/test_tutorial003.py @@ -45,7 +45,7 @@ "name": "Deadpond", }, "Team:", - {"id": 2, "name": "Z-Force", "headquarters": "Sister Margaret’s Bar"}, + {"id": 2, "name": "Z-Force", "headquarters": "Sister Margaret's Bar"}, ], [ "Hero:", diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial003_py310.py b/tests/test_tutorial/test_connect/test_select/test_tutorial003_py310.py index 69abccb0aa..5b710c4358 100644 --- a/tests/test_tutorial/test_connect/test_select/test_tutorial003_py310.py +++ b/tests/test_tutorial/test_connect/test_select/test_tutorial003_py310.py @@ -45,7 +45,7 @@ "name": "Deadpond", }, "Team:", - {"id": 2, "name": "Z-Force", "headquarters": "Sister Margaret’s Bar"}, + {"id": 2, "name": "Z-Force", "headquarters": "Sister Margaret's Bar"}, ], [ "Hero:", diff --git a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py index fb08b9a5fd..2c60ce6d04 100644 --- a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py @@ -13,7 +13,7 @@ def test_tutorial(clear_sqlmodel): with TestClient(mod.app) as client: team_preventers = {"name": "Preventers", "headquarters": "Sharp Tower"} - team_z_force = {"name": "Z-Force", "headquarters": "Sister Margaret’s Bar"} + team_z_force = {"name": "Z-Force", "headquarters": "Sister Margaret's Bar"} response = client.post("/teams/", json=team_preventers) assert response.status_code == 200, response.text team_preventers_data = response.json() diff --git a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py310.py index dae7db3378..045a66ba5d 100644 --- a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py310.py @@ -16,7 +16,7 @@ def test_tutorial(clear_sqlmodel): with TestClient(mod.app) as client: team_preventers = {"name": "Preventers", "headquarters": "Sharp Tower"} - team_z_force = {"name": "Z-Force", "headquarters": "Sister Margaret’s Bar"} + team_z_force = {"name": "Z-Force", "headquarters": "Sister Margaret's Bar"} response = client.post("/teams/", json=team_preventers) assert response.status_code == 200, response.text team_preventers_data = response.json() diff --git a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py39.py index 72dee33434..924d0b90af 100644 --- a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py39.py @@ -16,7 +16,7 @@ def test_tutorial(clear_sqlmodel): with TestClient(mod.app) as client: team_preventers = {"name": "Preventers", "headquarters": "Sharp Tower"} - team_z_force = {"name": "Z-Force", "headquarters": "Sister Margaret’s Bar"} + team_z_force = {"name": "Z-Force", "headquarters": "Sister Margaret's Bar"} response = client.post("/teams/", json=team_preventers) assert response.status_code == 200, response.text team_preventers_data = response.json() diff --git a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py index 42f87cef76..a1be7b094a 100644 --- a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py @@ -55,7 +55,7 @@ def test_tutorial(clear_sqlmodel): assert response.status_code == 404, response.text team_preventers = {"name": "Preventers", "headquarters": "Sharp Tower"} - team_z_force = {"name": "Z-Force", "headquarters": "Sister Margaret’s Bar"} + team_z_force = {"name": "Z-Force", "headquarters": "Sister Margaret's Bar"} response = client.post("/teams/", json=team_preventers) assert response.status_code == 200, response.text team_preventers_data = response.json() diff --git a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py310.py index 6cec87a0a7..882fcc796b 100644 --- a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py310.py @@ -58,7 +58,7 @@ def test_tutorial(clear_sqlmodel): assert response.status_code == 404, response.text team_preventers = {"name": "Preventers", "headquarters": "Sharp Tower"} - team_z_force = {"name": "Z-Force", "headquarters": "Sister Margaret’s Bar"} + team_z_force = {"name": "Z-Force", "headquarters": "Sister Margaret's Bar"} response = client.post("/teams/", json=team_preventers) assert response.status_code == 200, response.text team_preventers_data = response.json() diff --git a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py39.py index 70279f5b8d..12791b269c 100644 --- a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py39.py @@ -58,7 +58,7 @@ def test_tutorial(clear_sqlmodel): assert response.status_code == 404, response.text team_preventers = {"name": "Preventers", "headquarters": "Sharp Tower"} - team_z_force = {"name": "Z-Force", "headquarters": "Sister Margaret’s Bar"} + team_z_force = {"name": "Z-Force", "headquarters": "Sister Margaret's Bar"} response = client.post("/teams/", json=team_preventers) assert response.status_code == 200, response.text team_preventers_data = response.json() diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial001.py b/tests/test_tutorial/test_many_to_many/test_tutorial001.py index 7a3a028ca2..70bfe9a649 100644 --- a/tests/test_tutorial/test_many_to_many/test_tutorial001.py +++ b/tests/test_tutorial/test_many_to_many/test_tutorial001.py @@ -12,7 +12,7 @@ [ "Deadpond teams:", [ - {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret’s Bar"}, + {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret's Bar"}, {"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}, ], ], diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial001_py310.py b/tests/test_tutorial/test_many_to_many/test_tutorial001_py310.py index 19a1640bd6..bf31d9c695 100644 --- a/tests/test_tutorial/test_many_to_many/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_many_to_many/test_tutorial001_py310.py @@ -12,7 +12,7 @@ [ "Deadpond teams:", [ - {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret’s Bar"}, + {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret's Bar"}, {"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}, ], ], diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial001_py39.py b/tests/test_tutorial/test_many_to_many/test_tutorial001_py39.py index 23a7649f03..cb7a4d8456 100644 --- a/tests/test_tutorial/test_many_to_many/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_many_to_many/test_tutorial001_py39.py @@ -12,7 +12,7 @@ [ "Deadpond teams:", [ - {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret’s Bar"}, + {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret's Bar"}, {"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}, ], ], diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial002.py b/tests/test_tutorial/test_many_to_many/test_tutorial002.py index 4c99e0263b..d4d7d95e89 100644 --- a/tests/test_tutorial/test_many_to_many/test_tutorial002.py +++ b/tests/test_tutorial/test_many_to_many/test_tutorial002.py @@ -12,7 +12,7 @@ [ "Deadpond teams:", [ - {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret’s Bar"}, + {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret's Bar"}, {"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}, ], ], @@ -36,7 +36,7 @@ "Updated Spider-Boy's Teams:", [ {"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}, - {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret’s Bar"}, + {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret's Bar"}, ], ], [ diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial002_py310.py b/tests/test_tutorial/test_many_to_many/test_tutorial002_py310.py index d480190f6f..ad7c892fcd 100644 --- a/tests/test_tutorial/test_many_to_many/test_tutorial002_py310.py +++ b/tests/test_tutorial/test_many_to_many/test_tutorial002_py310.py @@ -12,7 +12,7 @@ [ "Deadpond teams:", [ - {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret’s Bar"}, + {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret's Bar"}, {"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}, ], ], @@ -36,7 +36,7 @@ "Updated Spider-Boy's Teams:", [ {"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}, - {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret’s Bar"}, + {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret's Bar"}, ], ], [ diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial002_py39.py b/tests/test_tutorial/test_many_to_many/test_tutorial002_py39.py index 85f649acdf..c0df48d73c 100644 --- a/tests/test_tutorial/test_many_to_many/test_tutorial002_py39.py +++ b/tests/test_tutorial/test_many_to_many/test_tutorial002_py39.py @@ -12,7 +12,7 @@ [ "Deadpond teams:", [ - {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret’s Bar"}, + {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret's Bar"}, {"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}, ], ], @@ -36,7 +36,7 @@ "Updated Spider-Boy's Teams:", [ {"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}, - {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret’s Bar"}, + {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret's Bar"}, ], ], [ diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial003.py b/tests/test_tutorial/test_many_to_many/test_tutorial003.py index 7da0d6758f..35489b01ce 100644 --- a/tests/test_tutorial/test_many_to_many/test_tutorial003.py +++ b/tests/test_tutorial/test_many_to_many/test_tutorial003.py @@ -51,7 +51,7 @@ ], [ "Spider-Boy team:", - {"headquarters": "Sister Margaret’s Bar", "id": 1, "name": "Z-Force"}, + {"headquarters": "Sister Margaret's Bar", "id": 1, "name": "Z-Force"}, "is training:", True, ], diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial003_py310.py b/tests/test_tutorial/test_many_to_many/test_tutorial003_py310.py index 29918b3584..78a699c741 100644 --- a/tests/test_tutorial/test_many_to_many/test_tutorial003_py310.py +++ b/tests/test_tutorial/test_many_to_many/test_tutorial003_py310.py @@ -51,7 +51,7 @@ ], [ "Spider-Boy team:", - {"headquarters": "Sister Margaret’s Bar", "id": 1, "name": "Z-Force"}, + {"headquarters": "Sister Margaret's Bar", "id": 1, "name": "Z-Force"}, "is training:", True, ], diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial003_py39.py b/tests/test_tutorial/test_many_to_many/test_tutorial003_py39.py index 5f1c87ddc3..8fed921d82 100644 --- a/tests/test_tutorial/test_many_to_many/test_tutorial003_py39.py +++ b/tests/test_tutorial/test_many_to_many/test_tutorial003_py39.py @@ -51,7 +51,7 @@ ], [ "Spider-Boy team:", - {"headquarters": "Sister Margaret’s Bar", "id": 1, "name": "Z-Force"}, + {"headquarters": "Sister Margaret's Bar", "id": 1, "name": "Z-Force"}, "is training:", True, ], From 6770b9fd894050fd70c2f443f56f66c980416fa9 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 4 Dec 2023 12:13:24 +0000 Subject: [PATCH 322/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 553b761194..7272ff3ce5 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏️ Fix typo, simplify single quote/apostrophe character in "Sister Margaret's" everywhere in the docs. PR [#721](https://github.com/tiangolo/sqlmodel/pull/721) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update config with new pymdown extensions. PR [#712](https://github.com/tiangolo/sqlmodel/pull/712) by [@tiangolo](https://github.com/tiangolo). * 🙈 Update gitignore, include all coverage files. PR [#711](https://github.com/tiangolo/sqlmodel/pull/711) by [@tiangolo](https://github.com/tiangolo). From 5b733b348d1a41f0c1e6569bb4d0b12beeb2b756 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 4 Dec 2023 13:15:10 +0100 Subject: [PATCH 323/906] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.0.?= =?UTF-8?q?13?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 7 ++++--- sqlmodel/__init__.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 7272ff3ce5..dcdd700717 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,9 +2,7 @@ ## Latest Changes -* ✏️ Fix typo, simplify single quote/apostrophe character in "Sister Margaret's" everywhere in the docs. PR [#721](https://github.com/tiangolo/sqlmodel/pull/721) by [@tiangolo](https://github.com/tiangolo). -* 🔧 Update config with new pymdown extensions. PR [#712](https://github.com/tiangolo/sqlmodel/pull/712) by [@tiangolo](https://github.com/tiangolo). -* 🙈 Update gitignore, include all coverage files. PR [#711](https://github.com/tiangolo/sqlmodel/pull/711) by [@tiangolo](https://github.com/tiangolo). +## 0.0.13 ### Fixes @@ -21,11 +19,14 @@ ### Docs +* ✏️ Fix typo, simplify single quote/apostrophe character in "Sister Margaret's" everywhere in the docs. PR [#721](https://github.com/tiangolo/sqlmodel/pull/721) by [@tiangolo](https://github.com/tiangolo). * 📝 Update docs for Decimal, use proper types. PR [#719](https://github.com/tiangolo/sqlmodel/pull/719) by [@tiangolo](https://github.com/tiangolo). * 📝 Add source examples for Python 3.9 and 3.10. PR [#715](https://github.com/tiangolo/sqlmodel/pull/715) by [@tiangolo](https://github.com/tiangolo). ### Internal +* 🙈 Update gitignore, include all coverage files. PR [#711](https://github.com/tiangolo/sqlmodel/pull/711) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update config with new pymdown extensions. PR [#712](https://github.com/tiangolo/sqlmodel/pull/712) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update docs build setup, add support for sponsors, add sponsor GOVCERT.LU. PR [#720](https://github.com/tiangolo/sqlmodel/pull/720) by [@tiangolo](https://github.com/tiangolo). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#697](https://github.com/tiangolo/sqlmodel/pull/697) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * 🔧 Show line numbers in docs during local development. PR [#714](https://github.com/tiangolo/sqlmodel/pull/714) by [@tiangolo](https://github.com/tiangolo). diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index 5b117a1c05..0062dfaeb7 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.0.12" +__version__ = "0.0.13" # Re-export from SQLAlchemy from sqlalchemy.engine import create_engine as create_engine From fa2f178b8abee3e58b490bbfc0c7a7df966a1fe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 4 Dec 2023 15:42:39 +0100 Subject: [PATCH 324/906] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20Pydan?= =?UTF-8?q?tic=20v2=20(while=20keeping=20support=20for=20v1=20if=20v2=20is?= =?UTF-8?q?=20not=20available),=20including=20initial=20work=20by=20AntonD?= =?UTF-8?q?eMeester=20(#722)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mohamed Farahat Co-authored-by: Stefan Borer Co-authored-by: Peter Landry Co-authored-by: Anton De Meester --- .github/workflows/test.yml | 11 +- docs/tutorial/fastapi/multiple-models.md | 12 +- docs/tutorial/fastapi/update.md | 15 +- docs/tutorial/index.md | 2 +- .../fastapi/app_testing/tutorial001/main.py | 4 +- .../app_testing/tutorial001_py310/main.py | 4 +- .../app_testing/tutorial001_py39/main.py | 4 +- .../tutorial/fastapi/delete/tutorial001.py | 4 +- .../fastapi/delete/tutorial001_py310.py | 4 +- .../fastapi/delete/tutorial001_py39.py | 4 +- .../fastapi/limit_and_offset/tutorial001.py | 2 +- .../limit_and_offset/tutorial001_py310.py | 2 +- .../limit_and_offset/tutorial001_py39.py | 2 +- .../fastapi/multiple_models/tutorial001.py | 2 +- .../multiple_models/tutorial001_py310.py | 2 +- .../multiple_models/tutorial001_py39.py | 2 +- .../fastapi/multiple_models/tutorial002.py | 2 +- .../multiple_models/tutorial002_py310.py | 2 +- .../multiple_models/tutorial002_py39.py | 2 +- .../tutorial/fastapi/read_one/tutorial001.py | 2 +- .../fastapi/read_one/tutorial001_py310.py | 2 +- .../fastapi/read_one/tutorial001_py39.py | 2 +- .../fastapi/relationships/tutorial001.py | 8 +- .../relationships/tutorial001_py310.py | 8 +- .../fastapi/relationships/tutorial001_py39.py | 8 +- .../session_with_dependency/tutorial001.py | 4 +- .../tutorial001_py310.py | 4 +- .../tutorial001_py39.py | 4 +- .../tutorial/fastapi/teams/tutorial001.py | 8 +- .../fastapi/teams/tutorial001_py310.py | 8 +- .../fastapi/teams/tutorial001_py39.py | 8 +- .../tutorial/fastapi/update/tutorial001.py | 4 +- .../fastapi/update/tutorial001_py310.py | 4 +- .../fastapi/update/tutorial001_py39.py | 4 +- pyproject.toml | 4 +- sqlmodel/_compat.py | 554 ++++++++++++++++++ sqlmodel/main.py | 523 +++++++++-------- tests/conftest.py | 8 +- tests/test_deprecations.py | 30 + tests/test_enums.py | 40 +- tests/test_field_sa_relationship.py | 4 +- tests/test_instance_no_args.py | 24 +- tests/test_main.py | 1 - tests/test_missing_type.py | 5 +- tests/test_nullable.py | 2 +- tests/test_query.py | 4 +- .../test_delete/test_tutorial001.py | 56 +- .../test_delete/test_tutorial001_py310.py | 56 +- .../test_delete/test_tutorial001_py39.py | 56 +- .../test_limit_and_offset/test_tutorial001.py | 23 +- .../test_tutorial001_py310.py | 23 +- .../test_tutorial001_py39.py | 23 +- .../test_multiple_models/test_tutorial001.py | 26 +- .../test_tutorial001_py310.py | 27 +- .../test_tutorial001_py39.py | 26 +- .../test_multiple_models/test_tutorial002.py | 26 +- .../test_tutorial002_py310.py | 26 +- .../test_tutorial002_py39.py | 26 +- .../test_read_one/test_tutorial001.py | 26 +- .../test_read_one/test_tutorial001_py310.py | 26 +- .../test_read_one/test_tutorial001_py39.py | 26 +- .../test_relationships/test_tutorial001.py | 157 ++++- .../test_tutorial001_py310.py | 157 ++++- .../test_tutorial001_py39.py | 157 ++++- .../test_response_model/test_tutorial001.py | 26 +- .../test_tutorial001_py310.py | 26 +- .../test_tutorial001_py39.py | 26 +- .../test_tutorial001.py | 56 +- .../test_tutorial001_py310.py | 56 +- .../test_tutorial001_py39.py | 56 +- .../test_simple_hero_api/test_tutorial001.py | 26 +- .../test_tutorial001_py310.py | 26 +- .../test_teams/test_tutorial001.py | 111 +++- .../test_teams/test_tutorial001_py310.py | 111 +++- .../test_teams/test_tutorial001_py39.py | 111 +++- .../test_update/test_tutorial001.py | 56 +- .../test_update/test_tutorial001_py310.py | 56 +- .../test_update/test_tutorial001_py39.py | 56 +- tests/test_validation.py | 36 +- 79 files changed, 2617 insertions(+), 520 deletions(-) create mode 100644 sqlmodel/_compat.py create mode 100644 tests/test_deprecations.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9f2688dff7..ade60f2559 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,6 +27,9 @@ jobs: - "3.10" - "3.11" - "3.12" + pydantic-version: + - pydantic-v1 + - pydantic-v2 fail-fast: false steps: @@ -57,9 +60,15 @@ jobs: - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: python -m poetry install + - name: Install Pydantic v1 + if: matrix.pydantic-version == 'pydantic-v1' + run: pip install "pydantic>=1.10.0,<2.0.0" + - name: Install Pydantic v2 + if: matrix.pydantic-version == 'pydantic-v2' + run: pip install "pydantic>=2.0.2,<3.0.0" - name: Lint # Do not run on Python 3.7 as mypy behaves differently - if: matrix.python-version != '3.7' + if: matrix.python-version != '3.7' && matrix.pydantic-version == 'pydantic-v2' run: python -m poetry run bash scripts/lint.sh - run: mkdir coverage - name: Test diff --git a/docs/tutorial/fastapi/multiple-models.md b/docs/tutorial/fastapi/multiple-models.md index d57d1bd9b4..3995daa650 100644 --- a/docs/tutorial/fastapi/multiple-models.md +++ b/docs/tutorial/fastapi/multiple-models.md @@ -175,15 +175,17 @@ Now we use the type annotation `HeroCreate` for the request JSON data in the `he # Code below omitted 👇 ``` -Then we create a new `Hero` (this is the actual **table** model that saves things to the database) using `Hero.from_orm()`. +Then we create a new `Hero` (this is the actual **table** model that saves things to the database) using `Hero.model_validate()`. -The method `.from_orm()` reads data from another object with attributes and creates a new instance of this class, in this case `Hero`. +The method `.model_validate()` reads data from another object with attributes (or a dict) and creates a new instance of this class, in this case `Hero`. -The alternative is `Hero.parse_obj()` that reads data from a dictionary. +In this case, we have a `HeroCreate` instance in the `hero` variable. This is an object with attributes, so we use `.model_validate()` to read those attributes. -But as in this case, we have a `HeroCreate` instance in the `hero` variable. This is an object with attributes, so we use `.from_orm()` to read those attributes. +/// tip +In versions of **SQLModel** before `0.0.14` you would use the method `.from_orm()`, but it is now deprecated and you should use `.model_validate()` instead. +/// -With this, we create a new `Hero` instance (the one for the database) and put it in the variable `db_hero` from the data in the `hero` variable that is the `HeroCreate` instance we received from the request. +We can now create a new `Hero` instance (the one for the database) and put it in the variable `db_hero` from the data in the `hero` variable that is the `HeroCreate` instance we received from the request. ```Python hl_lines="3" # Code above omitted 👆 diff --git a/docs/tutorial/fastapi/update.md b/docs/tutorial/fastapi/update.md index 27c413f387..cfcf8a98e7 100644 --- a/docs/tutorial/fastapi/update.md +++ b/docs/tutorial/fastapi/update.md @@ -90,7 +90,7 @@ So, we need to read the hero from the database, with the **same logic** we used The `HeroUpdate` model has all the fields with **default values**, because they all have defaults, they are all optional, which is what we want. -But that also means that if we just call `hero.dict()` we will get a dictionary that could potentially have several or all of those values with their defaults, for example: +But that also means that if we just call `hero.model_dump()` we will get a dictionary that could potentially have several or all of those values with their defaults, for example: ```Python { @@ -102,7 +102,7 @@ But that also means that if we just call `hero.dict()` we will get a dictionary And then, if we update the hero in the database with this data, we would be removing any existing values, and that's probably **not what the client intended**. -But fortunately Pydantic models (and so SQLModel models) have a parameter we can pass to the `.dict()` method for that: `exclude_unset=True`. +But fortunately Pydantic models (and so SQLModel models) have a parameter we can pass to the `.model_dump()` method for that: `exclude_unset=True`. This tells Pydantic to **not include** the values that were **not sent** by the client. Saying it another way, it would **only** include the values that were **sent by the client**. @@ -112,7 +112,7 @@ So, if the client sent a JSON with no values: {} ``` -Then the dictionary we would get in Python using `hero.dict(exclude_unset=True)` would be: +Then the dictionary we would get in Python using `hero.model_dump(exclude_unset=True)` would be: ```Python {} @@ -126,7 +126,7 @@ But if the client sent a JSON with: } ``` -Then the dictionary we would get in Python using `hero.dict(exclude_unset=True)` would be: +Then the dictionary we would get in Python using `hero.model_dump(exclude_unset=True)` would be: ```Python { @@ -152,6 +152,9 @@ Then we use that to get the data that was actually sent by the client: /// +/// tip +Before SQLModel 0.0.14, the method was called `hero.dict(exclude_unset=True)`, but it was renamed to `hero.model_dump(exclude_unset=True)` to be consistent with Pydantic v2. + ## Update the Hero in the Database Now that we have a **dictionary with the data sent by the client**, we can iterate for each one of the keys and the values, and then we set them in the database hero model `db_hero` using `setattr()`. @@ -208,7 +211,7 @@ So, if the client wanted to intentionally remove the `age` of a hero, they could } ``` -And when getting the data with `hero.dict(exclude_unset=True)`, we would get: +And when getting the data with `hero.model_dump(exclude_unset=True)`, we would get: ```Python { @@ -226,4 +229,4 @@ These are some of the advantages of Pydantic, that we can use with SQLModel. ## Recap -Using `.dict(exclude_unset=True)` in SQLModel models (and Pydantic models) we can easily update data **correctly**, even in the **edge cases**. 😎 +Using `.model_dump(exclude_unset=True)` in SQLModel models (and Pydantic models) we can easily update data **correctly**, even in the **edge cases**. 😎 diff --git a/docs/tutorial/index.md b/docs/tutorial/index.md index 1e78c9c4f7..9b0939b0c1 100644 --- a/docs/tutorial/index.md +++ b/docs/tutorial/index.md @@ -82,8 +82,8 @@ There's a chance that you have multiple Python versions installed. You might want to try with the specific versions, for example with: -* `python3.11` * `python3.12` +* `python3.11` * `python3.10` * `python3.9` diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py b/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py index 3f0602e4b4..7014a73918 100644 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py @@ -54,7 +54,7 @@ def on_startup(): @app.post("/heroes/", response_model=HeroRead) def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) @@ -87,7 +87,7 @@ def update_hero( db_hero = session.get(Hero, hero_id) if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") - hero_data = hero.dict(exclude_unset=True) + hero_data = hero.model_dump(exclude_unset=True) for key, value in hero_data.items(): setattr(db_hero, key, value) session.add(db_hero) diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/main.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/main.py index e8615d91df..cf1bbb7130 100644 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/main.py +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/main.py @@ -52,7 +52,7 @@ def on_startup(): @app.post("/heroes/", response_model=HeroRead) def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) @@ -85,7 +85,7 @@ def update_hero( db_hero = session.get(Hero, hero_id) if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") - hero_data = hero.dict(exclude_unset=True) + hero_data = hero.model_dump(exclude_unset=True) for key, value in hero_data.items(): setattr(db_hero, key, value) session.add(db_hero) diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/main.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/main.py index 9816e70eb0..9f428ab3e8 100644 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/main.py +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/main.py @@ -54,7 +54,7 @@ def on_startup(): @app.post("/heroes/", response_model=HeroRead) def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) @@ -87,7 +87,7 @@ def update_hero( db_hero = session.get(Hero, hero_id) if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") - hero_data = hero.dict(exclude_unset=True) + hero_data = hero.model_dump(exclude_unset=True) for key, value in hero_data.items(): setattr(db_hero, key, value) session.add(db_hero) diff --git a/docs_src/tutorial/fastapi/delete/tutorial001.py b/docs_src/tutorial/fastapi/delete/tutorial001.py index 3069fc5e87..532817360a 100644 --- a/docs_src/tutorial/fastapi/delete/tutorial001.py +++ b/docs_src/tutorial/fastapi/delete/tutorial001.py @@ -50,7 +50,7 @@ def on_startup(): @app.post("/heroes/", response_model=HeroRead) def create_hero(hero: HeroCreate): with Session(engine) as session: - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) @@ -79,7 +79,7 @@ def update_hero(hero_id: int, hero: HeroUpdate): db_hero = session.get(Hero, hero_id) if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") - hero_data = hero.dict(exclude_unset=True) + hero_data = hero.model_dump(exclude_unset=True) for key, value in hero_data.items(): setattr(db_hero, key, value) session.add(db_hero) diff --git a/docs_src/tutorial/fastapi/delete/tutorial001_py310.py b/docs_src/tutorial/fastapi/delete/tutorial001_py310.py index 5b2da0a0b1..45e2e1d515 100644 --- a/docs_src/tutorial/fastapi/delete/tutorial001_py310.py +++ b/docs_src/tutorial/fastapi/delete/tutorial001_py310.py @@ -48,7 +48,7 @@ def on_startup(): @app.post("/heroes/", response_model=HeroRead) def create_hero(hero: HeroCreate): with Session(engine) as session: - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) @@ -77,7 +77,7 @@ def update_hero(hero_id: int, hero: HeroUpdate): db_hero = session.get(Hero, hero_id) if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") - hero_data = hero.dict(exclude_unset=True) + hero_data = hero.model_dump(exclude_unset=True) for key, value in hero_data.items(): setattr(db_hero, key, value) session.add(db_hero) diff --git a/docs_src/tutorial/fastapi/delete/tutorial001_py39.py b/docs_src/tutorial/fastapi/delete/tutorial001_py39.py index 5f498cf136..12f6bc3f9b 100644 --- a/docs_src/tutorial/fastapi/delete/tutorial001_py39.py +++ b/docs_src/tutorial/fastapi/delete/tutorial001_py39.py @@ -50,7 +50,7 @@ def on_startup(): @app.post("/heroes/", response_model=HeroRead) def create_hero(hero: HeroCreate): with Session(engine) as session: - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) @@ -79,7 +79,7 @@ def update_hero(hero_id: int, hero: HeroUpdate): db_hero = session.get(Hero, hero_id) if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") - hero_data = hero.dict(exclude_unset=True) + hero_data = hero.model_dump(exclude_unset=True) for key, value in hero_data.items(): setattr(db_hero, key, value) session.add(db_hero) diff --git a/docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py b/docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py index 2b8739ca70..2352f39022 100644 --- a/docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py +++ b/docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py @@ -44,7 +44,7 @@ def on_startup(): @app.post("/heroes/", response_model=HeroRead) def create_hero(hero: HeroCreate): with Session(engine) as session: - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) diff --git a/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py310.py b/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py310.py index 874a6e8438..ad8ff95e3a 100644 --- a/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py310.py +++ b/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py310.py @@ -42,7 +42,7 @@ def on_startup(): @app.post("/heroes/", response_model=HeroRead) def create_hero(hero: HeroCreate): with Session(engine) as session: - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) diff --git a/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py39.py b/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py39.py index b63fa753ff..b1f7cdcb6a 100644 --- a/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py39.py +++ b/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py39.py @@ -44,7 +44,7 @@ def on_startup(): @app.post("/heroes/", response_model=HeroRead) def create_hero(hero: HeroCreate): with Session(engine) as session: - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) diff --git a/docs_src/tutorial/fastapi/multiple_models/tutorial001.py b/docs_src/tutorial/fastapi/multiple_models/tutorial001.py index df20123333..7f59ac6a1d 100644 --- a/docs_src/tutorial/fastapi/multiple_models/tutorial001.py +++ b/docs_src/tutorial/fastapi/multiple_models/tutorial001.py @@ -46,7 +46,7 @@ def on_startup(): @app.post("/heroes/", response_model=HeroRead) def create_hero(hero: HeroCreate): with Session(engine) as session: - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) diff --git a/docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py b/docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py index 13129f383f..ff12eff55c 100644 --- a/docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py +++ b/docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py @@ -44,7 +44,7 @@ def on_startup(): @app.post("/heroes/", response_model=HeroRead) def create_hero(hero: HeroCreate): with Session(engine) as session: - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) diff --git a/docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py b/docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py index 41a51f448d..977a1ac8db 100644 --- a/docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py +++ b/docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py @@ -46,7 +46,7 @@ def on_startup(): @app.post("/heroes/", response_model=HeroRead) def create_hero(hero: HeroCreate): with Session(engine) as session: - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) diff --git a/docs_src/tutorial/fastapi/multiple_models/tutorial002.py b/docs_src/tutorial/fastapi/multiple_models/tutorial002.py index 392c2c5829..fffbe72496 100644 --- a/docs_src/tutorial/fastapi/multiple_models/tutorial002.py +++ b/docs_src/tutorial/fastapi/multiple_models/tutorial002.py @@ -44,7 +44,7 @@ def on_startup(): @app.post("/heroes/", response_model=HeroRead) def create_hero(hero: HeroCreate): with Session(engine) as session: - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) diff --git a/docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py b/docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py index 3eda88b194..7373edff5e 100644 --- a/docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py +++ b/docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py @@ -42,7 +42,7 @@ def on_startup(): @app.post("/heroes/", response_model=HeroRead) def create_hero(hero: HeroCreate): with Session(engine) as session: - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) diff --git a/docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py b/docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py index 473fe5b832..1b4a512520 100644 --- a/docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py +++ b/docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py @@ -44,7 +44,7 @@ def on_startup(): @app.post("/heroes/", response_model=HeroRead) def create_hero(hero: HeroCreate): with Session(engine) as session: - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) diff --git a/docs_src/tutorial/fastapi/read_one/tutorial001.py b/docs_src/tutorial/fastapi/read_one/tutorial001.py index 4d66e471a5..f18426e74c 100644 --- a/docs_src/tutorial/fastapi/read_one/tutorial001.py +++ b/docs_src/tutorial/fastapi/read_one/tutorial001.py @@ -44,7 +44,7 @@ def on_startup(): @app.post("/heroes/", response_model=HeroRead) def create_hero(hero: HeroCreate): with Session(engine) as session: - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) diff --git a/docs_src/tutorial/fastapi/read_one/tutorial001_py310.py b/docs_src/tutorial/fastapi/read_one/tutorial001_py310.py index 8883570dc5..e8c7d49b99 100644 --- a/docs_src/tutorial/fastapi/read_one/tutorial001_py310.py +++ b/docs_src/tutorial/fastapi/read_one/tutorial001_py310.py @@ -42,7 +42,7 @@ def on_startup(): @app.post("/heroes/", response_model=HeroRead) def create_hero(hero: HeroCreate): with Session(engine) as session: - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) diff --git a/docs_src/tutorial/fastapi/read_one/tutorial001_py39.py b/docs_src/tutorial/fastapi/read_one/tutorial001_py39.py index 0ad7016687..4dc5702fb6 100644 --- a/docs_src/tutorial/fastapi/read_one/tutorial001_py39.py +++ b/docs_src/tutorial/fastapi/read_one/tutorial001_py39.py @@ -44,7 +44,7 @@ def on_startup(): @app.post("/heroes/", response_model=HeroRead) def create_hero(hero: HeroCreate): with Session(engine) as session: - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) diff --git a/docs_src/tutorial/fastapi/relationships/tutorial001.py b/docs_src/tutorial/fastapi/relationships/tutorial001.py index 8477e4a2a0..51339e2a20 100644 --- a/docs_src/tutorial/fastapi/relationships/tutorial001.py +++ b/docs_src/tutorial/fastapi/relationships/tutorial001.py @@ -92,7 +92,7 @@ def on_startup(): @app.post("/heroes/", response_model=HeroRead) def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) @@ -125,7 +125,7 @@ def update_hero( db_hero = session.get(Hero, hero_id) if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") - hero_data = hero.dict(exclude_unset=True) + hero_data = hero.model_dump(exclude_unset=True) for key, value in hero_data.items(): setattr(db_hero, key, value) session.add(db_hero) @@ -146,7 +146,7 @@ def delete_hero(*, session: Session = Depends(get_session), hero_id: int): @app.post("/teams/", response_model=TeamRead) def create_team(*, session: Session = Depends(get_session), team: TeamCreate): - db_team = Team.from_orm(team) + db_team = Team.model_validate(team) session.add(db_team) session.commit() session.refresh(db_team) @@ -182,7 +182,7 @@ def update_team( db_team = session.get(Team, team_id) if not db_team: raise HTTPException(status_code=404, detail="Team not found") - team_data = team.dict(exclude_unset=True) + team_data = team.model_dump(exclude_unset=True) for key, value in team_data.items(): setattr(db_team, key, value) session.add(db_team) diff --git a/docs_src/tutorial/fastapi/relationships/tutorial001_py310.py b/docs_src/tutorial/fastapi/relationships/tutorial001_py310.py index bec6a6f2e2..35257bd513 100644 --- a/docs_src/tutorial/fastapi/relationships/tutorial001_py310.py +++ b/docs_src/tutorial/fastapi/relationships/tutorial001_py310.py @@ -90,7 +90,7 @@ def on_startup(): @app.post("/heroes/", response_model=HeroRead) def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) @@ -123,7 +123,7 @@ def update_hero( db_hero = session.get(Hero, hero_id) if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") - hero_data = hero.dict(exclude_unset=True) + hero_data = hero.model_dump(exclude_unset=True) for key, value in hero_data.items(): setattr(db_hero, key, value) session.add(db_hero) @@ -144,7 +144,7 @@ def delete_hero(*, session: Session = Depends(get_session), hero_id: int): @app.post("/teams/", response_model=TeamRead) def create_team(*, session: Session = Depends(get_session), team: TeamCreate): - db_team = Team.from_orm(team) + db_team = Team.model_validate(team) session.add(db_team) session.commit() session.refresh(db_team) @@ -180,7 +180,7 @@ def update_team( db_team = session.get(Team, team_id) if not db_team: raise HTTPException(status_code=404, detail="Team not found") - team_data = team.dict(exclude_unset=True) + team_data = team.model_dump(exclude_unset=True) for key, value in team_data.items(): setattr(db_team, key, value) session.add(db_team) diff --git a/docs_src/tutorial/fastapi/relationships/tutorial001_py39.py b/docs_src/tutorial/fastapi/relationships/tutorial001_py39.py index 3893905519..6ceae130a3 100644 --- a/docs_src/tutorial/fastapi/relationships/tutorial001_py39.py +++ b/docs_src/tutorial/fastapi/relationships/tutorial001_py39.py @@ -92,7 +92,7 @@ def on_startup(): @app.post("/heroes/", response_model=HeroRead) def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) @@ -125,7 +125,7 @@ def update_hero( db_hero = session.get(Hero, hero_id) if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") - hero_data = hero.dict(exclude_unset=True) + hero_data = hero.model_dump(exclude_unset=True) for key, value in hero_data.items(): setattr(db_hero, key, value) session.add(db_hero) @@ -146,7 +146,7 @@ def delete_hero(*, session: Session = Depends(get_session), hero_id: int): @app.post("/teams/", response_model=TeamRead) def create_team(*, session: Session = Depends(get_session), team: TeamCreate): - db_team = Team.from_orm(team) + db_team = Team.model_validate(team) session.add(db_team) session.commit() session.refresh(db_team) @@ -182,7 +182,7 @@ def update_team( db_team = session.get(Team, team_id) if not db_team: raise HTTPException(status_code=404, detail="Team not found") - team_data = team.dict(exclude_unset=True) + team_data = team.model_dump(exclude_unset=True) for key, value in team_data.items(): setattr(db_team, key, value) session.add(db_team) diff --git a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py index 3f0602e4b4..7014a73918 100644 --- a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py +++ b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py @@ -54,7 +54,7 @@ def on_startup(): @app.post("/heroes/", response_model=HeroRead) def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) @@ -87,7 +87,7 @@ def update_hero( db_hero = session.get(Hero, hero_id) if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") - hero_data = hero.dict(exclude_unset=True) + hero_data = hero.model_dump(exclude_unset=True) for key, value in hero_data.items(): setattr(db_hero, key, value) session.add(db_hero) diff --git a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py index e8615d91df..cf1bbb7130 100644 --- a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py +++ b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py @@ -52,7 +52,7 @@ def on_startup(): @app.post("/heroes/", response_model=HeroRead) def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) @@ -85,7 +85,7 @@ def update_hero( db_hero = session.get(Hero, hero_id) if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") - hero_data = hero.dict(exclude_unset=True) + hero_data = hero.model_dump(exclude_unset=True) for key, value in hero_data.items(): setattr(db_hero, key, value) session.add(db_hero) diff --git a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py index 9816e70eb0..9f428ab3e8 100644 --- a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py +++ b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py @@ -54,7 +54,7 @@ def on_startup(): @app.post("/heroes/", response_model=HeroRead) def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) @@ -87,7 +87,7 @@ def update_hero( db_hero = session.get(Hero, hero_id) if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") - hero_data = hero.dict(exclude_unset=True) + hero_data = hero.model_dump(exclude_unset=True) for key, value in hero_data.items(): setattr(db_hero, key, value) session.add(db_hero) diff --git a/docs_src/tutorial/fastapi/teams/tutorial001.py b/docs_src/tutorial/fastapi/teams/tutorial001.py index 1da0dad8a2..785c525918 100644 --- a/docs_src/tutorial/fastapi/teams/tutorial001.py +++ b/docs_src/tutorial/fastapi/teams/tutorial001.py @@ -83,7 +83,7 @@ def on_startup(): @app.post("/heroes/", response_model=HeroRead) def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) @@ -116,7 +116,7 @@ def update_hero( db_hero = session.get(Hero, hero_id) if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") - hero_data = hero.dict(exclude_unset=True) + hero_data = hero.model_dump(exclude_unset=True) for key, value in hero_data.items(): setattr(db_hero, key, value) session.add(db_hero) @@ -137,7 +137,7 @@ def delete_hero(*, session: Session = Depends(get_session), hero_id: int): @app.post("/teams/", response_model=TeamRead) def create_team(*, session: Session = Depends(get_session), team: TeamCreate): - db_team = Team.from_orm(team) + db_team = Team.model_validate(team) session.add(db_team) session.commit() session.refresh(db_team) @@ -173,7 +173,7 @@ def update_team( db_team = session.get(Team, team_id) if not db_team: raise HTTPException(status_code=404, detail="Team not found") - team_data = team.dict(exclude_unset=True) + team_data = team.model_dump(exclude_unset=True) for key, value in team_data.items(): setattr(db_team, key, value) session.add(db_team) diff --git a/docs_src/tutorial/fastapi/teams/tutorial001_py310.py b/docs_src/tutorial/fastapi/teams/tutorial001_py310.py index a9a527df73..dea4bd8a9b 100644 --- a/docs_src/tutorial/fastapi/teams/tutorial001_py310.py +++ b/docs_src/tutorial/fastapi/teams/tutorial001_py310.py @@ -81,7 +81,7 @@ def on_startup(): @app.post("/heroes/", response_model=HeroRead) def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) @@ -114,7 +114,7 @@ def update_hero( db_hero = session.get(Hero, hero_id) if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") - hero_data = hero.dict(exclude_unset=True) + hero_data = hero.model_dump(exclude_unset=True) for key, value in hero_data.items(): setattr(db_hero, key, value) session.add(db_hero) @@ -135,7 +135,7 @@ def delete_hero(*, session: Session = Depends(get_session), hero_id: int): @app.post("/teams/", response_model=TeamRead) def create_team(*, session: Session = Depends(get_session), team: TeamCreate): - db_team = Team.from_orm(team) + db_team = Team.model_validate(team) session.add(db_team) session.commit() session.refresh(db_team) @@ -171,7 +171,7 @@ def update_team( db_team = session.get(Team, team_id) if not db_team: raise HTTPException(status_code=404, detail="Team not found") - team_data = team.dict(exclude_unset=True) + team_data = team.model_dump(exclude_unset=True) for key, value in team_data.items(): setattr(db_team, key, value) session.add(db_team) diff --git a/docs_src/tutorial/fastapi/teams/tutorial001_py39.py b/docs_src/tutorial/fastapi/teams/tutorial001_py39.py index 1a36428994..cc6429adcf 100644 --- a/docs_src/tutorial/fastapi/teams/tutorial001_py39.py +++ b/docs_src/tutorial/fastapi/teams/tutorial001_py39.py @@ -83,7 +83,7 @@ def on_startup(): @app.post("/heroes/", response_model=HeroRead) def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) @@ -116,7 +116,7 @@ def update_hero( db_hero = session.get(Hero, hero_id) if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") - hero_data = hero.dict(exclude_unset=True) + hero_data = hero.model_dump(exclude_unset=True) for key, value in hero_data.items(): setattr(db_hero, key, value) session.add(db_hero) @@ -137,7 +137,7 @@ def delete_hero(*, session: Session = Depends(get_session), hero_id: int): @app.post("/teams/", response_model=TeamRead) def create_team(*, session: Session = Depends(get_session), team: TeamCreate): - db_team = Team.from_orm(team) + db_team = Team.model_validate(team) session.add(db_team) session.commit() session.refresh(db_team) @@ -173,7 +173,7 @@ def update_team( db_team = session.get(Team, team_id) if not db_team: raise HTTPException(status_code=404, detail="Team not found") - team_data = team.dict(exclude_unset=True) + team_data = team.model_dump(exclude_unset=True) for key, value in team_data.items(): setattr(db_team, key, value) session.add(db_team) diff --git a/docs_src/tutorial/fastapi/update/tutorial001.py b/docs_src/tutorial/fastapi/update/tutorial001.py index bb98efd581..5639638d5c 100644 --- a/docs_src/tutorial/fastapi/update/tutorial001.py +++ b/docs_src/tutorial/fastapi/update/tutorial001.py @@ -50,7 +50,7 @@ def on_startup(): @app.post("/heroes/", response_model=HeroRead) def create_hero(hero: HeroCreate): with Session(engine) as session: - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) @@ -79,7 +79,7 @@ def update_hero(hero_id: int, hero: HeroUpdate): db_hero = session.get(Hero, hero_id) if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") - hero_data = hero.dict(exclude_unset=True) + hero_data = hero.model_dump(exclude_unset=True) for key, value in hero_data.items(): setattr(db_hero, key, value) session.add(db_hero) diff --git a/docs_src/tutorial/fastapi/update/tutorial001_py310.py b/docs_src/tutorial/fastapi/update/tutorial001_py310.py index 79069181fb..4faf266f84 100644 --- a/docs_src/tutorial/fastapi/update/tutorial001_py310.py +++ b/docs_src/tutorial/fastapi/update/tutorial001_py310.py @@ -48,7 +48,7 @@ def on_startup(): @app.post("/heroes/", response_model=HeroRead) def create_hero(hero: HeroCreate): with Session(engine) as session: - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) @@ -77,7 +77,7 @@ def update_hero(hero_id: int, hero: HeroUpdate): db_hero = session.get(Hero, hero_id) if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") - hero_data = hero.dict(exclude_unset=True) + hero_data = hero.model_dump(exclude_unset=True) for key, value in hero_data.items(): setattr(db_hero, key, value) session.add(db_hero) diff --git a/docs_src/tutorial/fastapi/update/tutorial001_py39.py b/docs_src/tutorial/fastapi/update/tutorial001_py39.py index c788eb1c7a..b0daa87880 100644 --- a/docs_src/tutorial/fastapi/update/tutorial001_py39.py +++ b/docs_src/tutorial/fastapi/update/tutorial001_py39.py @@ -50,7 +50,7 @@ def on_startup(): @app.post("/heroes/", response_model=HeroRead) def create_hero(hero: HeroCreate): with Session(engine) as session: - db_hero = Hero.from_orm(hero) + db_hero = Hero.model_validate(hero) session.add(db_hero) session.commit() session.refresh(db_hero) @@ -79,7 +79,7 @@ def update_hero(hero_id: int, hero: HeroUpdate): db_hero = session.get(Hero, hero_id) if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") - hero_data = hero.dict(exclude_unset=True) + hero_data = hero.model_dump(exclude_unset=True) for key, value in hero_data.items(): setattr(db_hero, key, value) session.add(db_hero) diff --git a/pyproject.toml b/pyproject.toml index 24a6c5c22e..10d73793d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ classifiers = [ [tool.poetry.dependencies] python = "^3.7" SQLAlchemy = ">=2.0.0,<2.1.0" -pydantic = "^1.9.0" +pydantic = ">=1.10.13,<3.0.0" [tool.poetry.group.dev.dependencies] pytest = "^7.0.1" @@ -50,6 +50,8 @@ fastapi = "^0.103.2" ruff = "^0.1.2" # For FastAPI tests httpx = "0.24.1" +# TODO: upgrade when deprecating Python 3.7 +dirty-equals = "^0.6.0" typer-cli = "^0.0.13" mkdocs-markdownextradata-plugin = ">=0.1.7,<0.3.0" diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py new file mode 100644 index 0000000000..2a2caca3e8 --- /dev/null +++ b/sqlmodel/_compat.py @@ -0,0 +1,554 @@ +import types +from contextlib import contextmanager +from contextvars import ContextVar +from dataclasses import dataclass +from typing import ( + TYPE_CHECKING, + AbstractSet, + Any, + Dict, + ForwardRef, + Generator, + Mapping, + Optional, + Set, + Type, + TypeVar, + Union, +) + +from pydantic import VERSION as PYDANTIC_VERSION +from pydantic.fields import FieldInfo +from typing_extensions import get_args, get_origin + +IS_PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.") + + +if TYPE_CHECKING: + from .main import RelationshipInfo, SQLModel + +UnionType = getattr(types, "UnionType", Union) +NoneType = type(None) +T = TypeVar("T") +InstanceOrType = Union[T, Type[T]] +_TSQLModel = TypeVar("_TSQLModel", bound="SQLModel") + + +class FakeMetadata: + max_length: Optional[int] = None + max_digits: Optional[int] = None + decimal_places: Optional[int] = None + + +@dataclass +class ObjectWithUpdateWrapper: + obj: Any + update: Dict[str, Any] + + def __getattribute__(self, __name: str) -> Any: + if __name in self.update: + return self.update[__name] + return getattr(self.obj, __name) + + +def _is_union_type(t: Any) -> bool: + return t is UnionType or t is Union + + +finish_init: ContextVar[bool] = ContextVar("finish_init", default=True) + + +@contextmanager +def partial_init() -> Generator[None, None, None]: + token = finish_init.set(False) + yield + finish_init.reset(token) + + +if IS_PYDANTIC_V2: + from pydantic import ConfigDict as BaseConfig + from pydantic._internal._fields import PydanticMetadata + from pydantic._internal._model_construction import ModelMetaclass + from pydantic._internal._repr import Representation as Representation + from pydantic_core import PydanticUndefined as Undefined + from pydantic_core import PydanticUndefinedType as UndefinedType + + # Dummy for types, to make it importable + class ModelField: + pass + + class SQLModelConfig(BaseConfig, total=False): + table: Optional[bool] + registry: Optional[Any] + + def get_config_value( + *, model: InstanceOrType["SQLModel"], parameter: str, default: Any = None + ) -> Any: + return model.model_config.get(parameter, default) + + def set_config_value( + *, + model: InstanceOrType["SQLModel"], + parameter: str, + value: Any, + ) -> None: + model.model_config[parameter] = value # type: ignore[literal-required] + + def get_model_fields(model: InstanceOrType["SQLModel"]) -> Dict[str, "FieldInfo"]: + return model.model_fields + + def set_fields_set( + new_object: InstanceOrType["SQLModel"], fields: Set["FieldInfo"] + ) -> None: + object.__setattr__(new_object, "__pydantic_fields_set__", fields) + + def get_annotations(class_dict: Dict[str, Any]) -> Dict[str, Any]: + return class_dict.get("__annotations__", {}) + + def is_table_model_class(cls: Type[Any]) -> bool: + config = getattr(cls, "model_config", {}) + if config: + return config.get("table", False) or False + return False + + def get_relationship_to( + name: str, + rel_info: "RelationshipInfo", + annotation: Any, + ) -> Any: + origin = get_origin(annotation) + use_annotation = annotation + # Direct relationships (e.g. 'Team' or Team) have None as an origin + if origin is None: + if isinstance(use_annotation, ForwardRef): + use_annotation = use_annotation.__forward_arg__ + else: + return use_annotation + # If Union (e.g. Optional), get the real field + elif _is_union_type(origin): + use_annotation = get_args(annotation) + if len(use_annotation) > 2: + raise ValueError( + "Cannot have a (non-optional) union as a SQLAlchemy field" + ) + arg1, arg2 = use_annotation + if arg1 is NoneType and arg2 is not NoneType: + use_annotation = arg2 + elif arg2 is NoneType and arg1 is not NoneType: + use_annotation = arg1 + else: + raise ValueError( + "Cannot have a Union of None and None as a SQLAlchemy field" + ) + + # If a list, then also get the real field + elif origin is list: + use_annotation = get_args(annotation)[0] + + return get_relationship_to( + name=name, rel_info=rel_info, annotation=use_annotation + ) + + def is_field_noneable(field: "FieldInfo") -> bool: + if getattr(field, "nullable", Undefined) is not Undefined: + return field.nullable # type: ignore + origin = get_origin(field.annotation) + if origin is not None and _is_union_type(origin): + args = get_args(field.annotation) + if any(arg is NoneType for arg in args): + return True + if not field.is_required(): + if field.default is Undefined: + return False + if field.annotation is None or field.annotation is NoneType: # type: ignore[comparison-overlap] + return True + return False + return False + + def get_type_from_field(field: Any) -> Any: + type_: Any = field.annotation + # Resolve Optional fields + if type_ is None: + raise ValueError("Missing field type") + origin = get_origin(type_) + if origin is None: + return type_ + if _is_union_type(origin): + bases = get_args(type_) + if len(bases) > 2: + raise ValueError( + "Cannot have a (non-optional) union as a SQLAlchemy field" + ) + # Non optional unions are not allowed + if bases[0] is not NoneType and bases[1] is not NoneType: + raise ValueError( + "Cannot have a (non-optional) union as a SQLlchemy field" + ) + # Optional unions are allowed + return bases[0] if bases[0] is not NoneType else bases[1] + return origin + + def get_field_metadata(field: Any) -> Any: + for meta in field.metadata: + if isinstance(meta, PydanticMetadata): + return meta + return FakeMetadata() + + def post_init_field_info(field_info: FieldInfo) -> None: + return None + + # Dummy to make it importable + def _calculate_keys( + self: "SQLModel", + include: Optional[Mapping[Union[int, str], Any]], + exclude: Optional[Mapping[Union[int, str], Any]], + exclude_unset: bool, + update: Optional[Dict[str, Any]] = None, + ) -> Optional[AbstractSet[str]]: # pragma: no cover + return None + + def sqlmodel_table_construct( + *, + self_instance: _TSQLModel, + values: Dict[str, Any], + _fields_set: Union[Set[str], None] = None, + ) -> _TSQLModel: + # Copy from Pydantic's BaseModel.construct() + # Ref: https://github.com/pydantic/pydantic/blob/v2.5.2/pydantic/main.py#L198 + # Modified to not include everything, only the model fields, and to + # set relationships + # SQLModel override to get class SQLAlchemy __dict__ attributes and + # set them back in after creating the object + # new_obj = cls.__new__(cls) + cls = type(self_instance) + old_dict = self_instance.__dict__.copy() + # End SQLModel override + + fields_values: Dict[str, Any] = {} + defaults: Dict[ + str, Any + ] = {} # keeping this separate from `fields_values` helps us compute `_fields_set` + for name, field in cls.model_fields.items(): + if field.alias and field.alias in values: + fields_values[name] = values.pop(field.alias) + elif name in values: + fields_values[name] = values.pop(name) + elif not field.is_required(): + defaults[name] = field.get_default(call_default_factory=True) + if _fields_set is None: + _fields_set = set(fields_values.keys()) + fields_values.update(defaults) + + _extra: Union[Dict[str, Any], None] = None + if cls.model_config.get("extra") == "allow": + _extra = {} + for k, v in values.items(): + _extra[k] = v + # SQLModel override, do not include everything, only the model fields + # else: + # fields_values.update(values) + # End SQLModel override + # SQLModel override + # Do not set __dict__, instead use setattr to trigger SQLAlchemy + # object.__setattr__(new_obj, "__dict__", fields_values) + # instrumentation + for key, value in {**old_dict, **fields_values}.items(): + setattr(self_instance, key, value) + # End SQLModel override + object.__setattr__(self_instance, "__pydantic_fields_set__", _fields_set) + if not cls.__pydantic_root_model__: + object.__setattr__(self_instance, "__pydantic_extra__", _extra) + + if cls.__pydantic_post_init__: + self_instance.model_post_init(None) + elif not cls.__pydantic_root_model__: + # Note: if there are any private attributes, cls.__pydantic_post_init__ would exist + # Since it doesn't, that means that `__pydantic_private__` should be set to None + object.__setattr__(self_instance, "__pydantic_private__", None) + # SQLModel override, set relationships + # Get and set any relationship objects + for key in self_instance.__sqlmodel_relationships__: + value = values.get(key, Undefined) + if value is not Undefined: + setattr(self_instance, key, value) + # End SQLModel override + return self_instance + + def sqlmodel_validate( + cls: Type[_TSQLModel], + obj: Any, + *, + strict: Union[bool, None] = None, + from_attributes: Union[bool, None] = None, + context: Union[Dict[str, Any], None] = None, + update: Union[Dict[str, Any], None] = None, + ) -> _TSQLModel: + if not is_table_model_class(cls): + new_obj: _TSQLModel = cls.__new__(cls) + else: + # If table, create the new instance normally to make SQLAlchemy create + # the _sa_instance_state attribute + # The wrapper of this function should use with _partial_init() + with partial_init(): + new_obj = cls() + # SQLModel Override to get class SQLAlchemy __dict__ attributes and + # set them back in after creating the object + old_dict = new_obj.__dict__.copy() + use_obj = obj + if isinstance(obj, dict) and update: + use_obj = {**obj, **update} + elif update: + use_obj = ObjectWithUpdateWrapper(obj=obj, update=update) + cls.__pydantic_validator__.validate_python( + use_obj, + strict=strict, + from_attributes=from_attributes, + context=context, + self_instance=new_obj, + ) + # Capture fields set to restore it later + fields_set = new_obj.__pydantic_fields_set__.copy() + if not is_table_model_class(cls): + # If not table, normal Pydantic code, set __dict__ + new_obj.__dict__ = {**old_dict, **new_obj.__dict__} + else: + # Do not set __dict__, instead use setattr to trigger SQLAlchemy + # instrumentation + for key, value in {**old_dict, **new_obj.__dict__}.items(): + setattr(new_obj, key, value) + # Restore fields set + object.__setattr__(new_obj, "__pydantic_fields_set__", fields_set) + # Get and set any relationship objects + if is_table_model_class(cls): + for key in new_obj.__sqlmodel_relationships__: + value = getattr(use_obj, key, Undefined) + if value is not Undefined: + setattr(new_obj, key, value) + return new_obj + + def sqlmodel_init(*, self: "SQLModel", data: Dict[str, Any]) -> None: + old_dict = self.__dict__.copy() + if not is_table_model_class(self.__class__): + self.__pydantic_validator__.validate_python( + data, + self_instance=self, + ) + else: + sqlmodel_table_construct( + self_instance=self, + values=data, + ) + object.__setattr__( + self, + "__dict__", + {**old_dict, **self.__dict__}, + ) + +else: + from pydantic import BaseConfig as BaseConfig # type: ignore[assignment] + from pydantic.errors import ConfigError + from pydantic.fields import ( # type: ignore[attr-defined, no-redef] + SHAPE_SINGLETON, + ModelField, + ) + from pydantic.fields import ( # type: ignore[attr-defined, no-redef] + Undefined as Undefined, # noqa + ) + from pydantic.fields import ( # type: ignore[attr-defined, no-redef] + UndefinedType as UndefinedType, + ) + from pydantic.main import ( # type: ignore[no-redef] + ModelMetaclass as ModelMetaclass, + ) + from pydantic.main import validate_model + from pydantic.typing import resolve_annotations + from pydantic.utils import ROOT_KEY, ValueItems + from pydantic.utils import ( # type: ignore[no-redef] + Representation as Representation, + ) + + class SQLModelConfig(BaseConfig): # type: ignore[no-redef] + table: Optional[bool] = None # type: ignore[misc] + registry: Optional[Any] = None # type: ignore[misc] + + def get_config_value( + *, model: InstanceOrType["SQLModel"], parameter: str, default: Any = None + ) -> Any: + return getattr(model.__config__, parameter, default) # type: ignore[union-attr] + + def set_config_value( + *, + model: InstanceOrType["SQLModel"], + parameter: str, + value: Any, + ) -> None: + setattr(model.__config__, parameter, value) # type: ignore + + def get_model_fields(model: InstanceOrType["SQLModel"]) -> Dict[str, "FieldInfo"]: + return model.__fields__ # type: ignore + + def set_fields_set( + new_object: InstanceOrType["SQLModel"], fields: Set["FieldInfo"] + ) -> None: + object.__setattr__(new_object, "__fields_set__", fields) + + def get_annotations(class_dict: Dict[str, Any]) -> Dict[str, Any]: + return resolve_annotations( # type: ignore[no-any-return] + class_dict.get("__annotations__", {}), + class_dict.get("__module__", None), + ) + + def is_table_model_class(cls: Type[Any]) -> bool: + config = getattr(cls, "__config__", None) + if config: + return getattr(config, "table", False) + return False + + def get_relationship_to( + name: str, + rel_info: "RelationshipInfo", + annotation: Any, + ) -> Any: + temp_field = ModelField.infer( # type: ignore[attr-defined] + name=name, + value=rel_info, + annotation=annotation, + class_validators=None, + config=SQLModelConfig, + ) + relationship_to = temp_field.type_ + if isinstance(temp_field.type_, ForwardRef): + relationship_to = temp_field.type_.__forward_arg__ + return relationship_to + + def is_field_noneable(field: "FieldInfo") -> bool: + if not field.required: # type: ignore[attr-defined] + # Taken from [Pydantic](https://github.com/samuelcolvin/pydantic/blob/v1.8.2/pydantic/fields.py#L946-L947) + return field.allow_none and ( # type: ignore[attr-defined] + field.shape != SHAPE_SINGLETON or not field.sub_fields # type: ignore[attr-defined] + ) + return field.allow_none # type: ignore[no-any-return, attr-defined] + + def get_type_from_field(field: Any) -> Any: + if isinstance(field.type_, type) and field.shape == SHAPE_SINGLETON: + return field.type_ + raise ValueError(f"The field {field.name} has no matching SQLAlchemy type") + + def get_field_metadata(field: Any) -> Any: + metadata = FakeMetadata() + metadata.max_length = field.field_info.max_length + metadata.max_digits = getattr(field.type_, "max_digits", None) + metadata.decimal_places = getattr(field.type_, "decimal_places", None) + return metadata + + def post_init_field_info(field_info: FieldInfo) -> None: + field_info._validate() # type: ignore[attr-defined] + + def _calculate_keys( + self: "SQLModel", + include: Optional[Mapping[Union[int, str], Any]], + exclude: Optional[Mapping[Union[int, str], Any]], + exclude_unset: bool, + update: Optional[Dict[str, Any]] = None, + ) -> Optional[AbstractSet[str]]: + if include is None and exclude is None and not exclude_unset: + # Original in Pydantic: + # return None + # Updated to not return SQLAlchemy attributes + # Do not include relationships as that would easily lead to infinite + # recursion, or traversing the whole database + return ( + self.__fields__.keys() # noqa + ) # | self.__sqlmodel_relationships__.keys() + + keys: AbstractSet[str] + if exclude_unset: + keys = self.__fields_set__.copy() # noqa + else: + # Original in Pydantic: + # keys = self.__dict__.keys() + # Updated to not return SQLAlchemy attributes + # Do not include relationships as that would easily lead to infinite + # recursion, or traversing the whole database + keys = ( + self.__fields__.keys() # noqa + ) # | self.__sqlmodel_relationships__.keys() + if include is not None: + keys &= include.keys() + + if update: + keys -= update.keys() + + if exclude: + keys -= {k for k, v in exclude.items() if ValueItems.is_true(v)} + + return keys + + def sqlmodel_validate( + cls: Type[_TSQLModel], + obj: Any, + *, + strict: Union[bool, None] = None, + from_attributes: Union[bool, None] = None, + context: Union[Dict[str, Any], None] = None, + update: Union[Dict[str, Any], None] = None, + ) -> _TSQLModel: + # This was SQLModel's original from_orm() for Pydantic v1 + # Duplicated from Pydantic + if not cls.__config__.orm_mode: # type: ignore[attr-defined] # noqa + raise ConfigError( + "You must have the config attribute orm_mode=True to use from_orm" + ) + if not isinstance(obj, Mapping): + obj = ( + {ROOT_KEY: obj} + if cls.__custom_root_type__ # type: ignore[attr-defined] # noqa + else cls._decompose_class(obj) # type: ignore[attr-defined] # noqa + ) + # SQLModel, support update dict + if update is not None: + obj = {**obj, **update} + # End SQLModel support dict + if not getattr(cls.__config__, "table", False): # noqa + # If not table, normal Pydantic code + m: _TSQLModel = cls.__new__(cls) + else: + # If table, create the new instance normally to make SQLAlchemy create + # the _sa_instance_state attribute + m = cls() + values, fields_set, validation_error = validate_model(cls, obj) + if validation_error: + raise validation_error + # Updated to trigger SQLAlchemy internal handling + if not getattr(cls.__config__, "table", False): # noqa + object.__setattr__(m, "__dict__", values) + else: + for key, value in values.items(): + setattr(m, key, value) + # Continue with standard Pydantic logic + object.__setattr__(m, "__fields_set__", fields_set) + m._init_private_attributes() # type: ignore[attr-defined] # noqa + return m + + def sqlmodel_init(*, self: "SQLModel", data: Dict[str, Any]) -> None: + values, fields_set, validation_error = validate_model(self.__class__, data) + # Only raise errors if not a SQLModel model + if ( + not is_table_model_class(self.__class__) # noqa + and validation_error + ): + raise validation_error + if not is_table_model_class(self.__class__): + object.__setattr__(self, "__dict__", values) + else: + # Do not set values as in Pydantic, pass them through setattr, so + # SQLAlchemy can handle them + for key, value in values.items(): + setattr(self, key, value) + object.__setattr__(self, "__fields_set__", fields_set) + non_pydantic_keys = data.keys() - values.keys() + + if is_table_model_class(self.__class__): + for key in non_pydantic_keys: + if key in self.__sqlmodel_relationships__: + setattr(self, key, data[key]) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index c30af5779f..10064c7116 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -11,7 +11,6 @@ Callable, ClassVar, Dict, - ForwardRef, List, Mapping, Optional, @@ -25,13 +24,8 @@ overload, ) -from pydantic import BaseConfig, BaseModel -from pydantic.errors import ConfigError, DictError -from pydantic.fields import SHAPE_SINGLETON, ModelField, Undefined, UndefinedType +from pydantic import BaseModel from pydantic.fields import FieldInfo as PydanticFieldInfo -from pydantic.main import ModelMetaclass, validate_model -from pydantic.typing import NoArgAnyCallable, resolve_annotations -from pydantic.utils import ROOT_KEY, Representation from sqlalchemy import ( Boolean, Column, @@ -57,11 +51,38 @@ from sqlalchemy.orm.instrumentation import is_instrumented from sqlalchemy.sql.schema import MetaData from sqlalchemy.sql.sqltypes import LargeBinary, Time -from typing_extensions import get_origin - +from typing_extensions import Literal, deprecated, get_origin + +from ._compat import ( # type: ignore[attr-defined] + IS_PYDANTIC_V2, + BaseConfig, + ModelField, + ModelMetaclass, + Representation, + SQLModelConfig, + Undefined, + UndefinedType, + _calculate_keys, + finish_init, + get_annotations, + get_config_value, + get_field_metadata, + get_model_fields, + get_relationship_to, + get_type_from_field, + is_field_noneable, + is_table_model_class, + post_init_field_info, + set_config_value, + set_fields_set, + sqlmodel_init, + sqlmodel_validate, +) from .sql.sqltypes import GUID, AutoString _T = TypeVar("_T") +NoArgAnyCallable = Callable[[], Any] +IncEx = Union[Set[int], Set[str], Dict[int, Any], Dict[str, Any], None] def __dataclass_transform__( @@ -321,7 +342,7 @@ def Field( sa_column_kwargs=sa_column_kwargs, **current_schema_extra, ) - field_info._validate() + post_init_field_info(field_info) return field_info @@ -341,7 +362,7 @@ def Relationship( *, back_populates: Optional[str] = None, link_model: Optional[Any] = None, - sa_relationship: Optional[RelationshipProperty] = None, # type: ignore + sa_relationship: Optional[RelationshipProperty[Any]] = None, ) -> Any: ... @@ -350,7 +371,7 @@ def Relationship( *, back_populates: Optional[str] = None, link_model: Optional[Any] = None, - sa_relationship: Optional[RelationshipProperty] = None, # type: ignore + sa_relationship: Optional[RelationshipProperty[Any]] = None, sa_relationship_args: Optional[Sequence[Any]] = None, sa_relationship_kwargs: Optional[Mapping[str, Any]] = None, ) -> Any: @@ -367,18 +388,20 @@ def Relationship( @__dataclass_transform__(kw_only_default=True, field_descriptors=(Field, FieldInfo)) class SQLModelMetaclass(ModelMetaclass, DeclarativeMeta): __sqlmodel_relationships__: Dict[str, RelationshipInfo] - __config__: Type[BaseConfig] - __fields__: Dict[str, ModelField] + model_config: SQLModelConfig + model_fields: Dict[str, FieldInfo] + __config__: Type[SQLModelConfig] + __fields__: Dict[str, ModelField] # type: ignore[assignment] # Replicate SQLAlchemy def __setattr__(cls, name: str, value: Any) -> None: - if getattr(cls.__config__, "table", False): + if is_table_model_class(cls): DeclarativeMeta.__setattr__(cls, name, value) else: super().__setattr__(name, value) def __delattr__(cls, name: str) -> None: - if getattr(cls.__config__, "table", False): + if is_table_model_class(cls): DeclarativeMeta.__delattr__(cls, name) else: super().__delattr__(name) @@ -393,9 +416,7 @@ def __new__( ) -> Any: relationships: Dict[str, RelationshipInfo] = {} dict_for_pydantic = {} - original_annotations = resolve_annotations( - class_dict.get("__annotations__", {}), class_dict.get("__module__", None) - ) + original_annotations = get_annotations(class_dict) pydantic_annotations = {} relationship_annotations = {} for k, v in class_dict.items(): @@ -424,10 +445,8 @@ def __new__( key.startswith("__") and key.endswith("__") ) # skip dunder methods and attributes } - pydantic_kwargs = kwargs.copy() config_kwargs = { - key: pydantic_kwargs.pop(key) - for key in pydantic_kwargs.keys() & allowed_config_kwargs + key: kwargs[key] for key in kwargs.keys() & allowed_config_kwargs } new_cls = super().__new__(cls, name, bases, dict_used, **config_kwargs) new_cls.__annotations__ = { @@ -437,7 +456,9 @@ def __new__( } def get_config(name: str) -> Any: - config_class_value = getattr(new_cls.__config__, name, Undefined) + config_class_value = get_config_value( + model=new_cls, parameter=name, default=Undefined + ) if config_class_value is not Undefined: return config_class_value kwarg_value = kwargs.get(name, Undefined) @@ -448,22 +469,27 @@ def get_config(name: str) -> Any: config_table = get_config("table") if config_table is True: # If it was passed by kwargs, ensure it's also set in config - new_cls.__config__.table = config_table - for k, v in new_cls.__fields__.items(): + set_config_value(model=new_cls, parameter="table", value=config_table) + for k, v in get_model_fields(new_cls).items(): col = get_column_from_field(v) setattr(new_cls, k, col) # Set a config flag to tell FastAPI that this should be read with a field # in orm_mode instead of preemptively converting it to a dict. - # This could be done by reading new_cls.__config__.table in FastAPI, but + # This could be done by reading new_cls.model_config['table'] in FastAPI, but # that's very specific about SQLModel, so let's have another config that # other future tools based on Pydantic can use. - new_cls.__config__.read_with_orm_mode = True + set_config_value( + model=new_cls, parameter="read_from_attributes", value=True + ) + # For compatibility with older versions + # TODO: remove this in the future + set_config_value(model=new_cls, parameter="read_with_orm_mode", value=True) config_registry = get_config("registry") if config_registry is not Undefined: config_registry = cast(registry, config_registry) # If it was passed by kwargs, ensure it's also set in config - new_cls.__config__.registry = config_table + set_config_value(model=new_cls, parameter="registry", value=config_table) setattr(new_cls, "_sa_registry", config_registry) # noqa: B010 setattr(new_cls, "metadata", config_registry.metadata) # noqa: B010 setattr(new_cls, "__abstract__", True) # noqa: B010 @@ -477,13 +503,8 @@ def __init__( # this allows FastAPI cloning a SQLModel for the response_model without # trying to create a new SQLAlchemy, for a new table, with the same name, that # triggers an error - base_is_table = False - for base in bases: - config = getattr(base, "__config__") # noqa: B009 - if config and getattr(config, "table", False): - base_is_table = True - break - if getattr(cls.__config__, "table", False) and not base_is_table: + base_is_table = any(is_table_model_class(base) for base in bases) + if is_table_model_class(cls) and not base_is_table: for rel_name, rel_info in cls.__sqlmodel_relationships__.items(): if rel_info.sa_relationship: # There's a SQLAlchemy relationship declared, that takes precedence @@ -500,16 +521,9 @@ def __init__( # handled well by SQLAlchemy without Mapped, so, wrap the # annotations in Mapped here cls.__annotations__[rel_name] = Mapped[ann] # type: ignore[valid-type] - temp_field = ModelField.infer( - name=rel_name, - value=rel_info, - annotation=ann, - class_validators=None, - config=BaseConfig, + relationship_to = get_relationship_to( + name=rel_name, rel_info=rel_info, annotation=ann ) - relationship_to = temp_field.type_ - if isinstance(temp_field.type_, ForwardRef): - relationship_to = temp_field.type_.__forward_arg__ rel_kwargs: Dict[str, Any] = {} if rel_info.back_populates: rel_kwargs["back_populates"] = rel_info.back_populates @@ -537,77 +551,89 @@ def __init__( ModelMetaclass.__init__(cls, classname, bases, dict_, **kw) -def get_sqlalchemy_type(field: ModelField) -> Any: - sa_type = getattr(field.field_info, "sa_type", Undefined) # noqa: B009 +def get_sqlalchemy_type(field: Any) -> Any: + if IS_PYDANTIC_V2: + field_info = field + else: + field_info = field.field_info + sa_type = getattr(field_info, "sa_type", Undefined) # noqa: B009 if sa_type is not Undefined: return sa_type - if isinstance(field.type_, type) and field.shape == SHAPE_SINGLETON: - # Check enums first as an enum can also be a str, needed by Pydantic/FastAPI - if issubclass(field.type_, Enum): - return sa_Enum(field.type_) - if issubclass(field.type_, str): - if field.field_info.max_length: - return AutoString(length=field.field_info.max_length) - return AutoString - if issubclass(field.type_, float): - return Float - if issubclass(field.type_, bool): - return Boolean - if issubclass(field.type_, int): - return Integer - if issubclass(field.type_, datetime): - return DateTime - if issubclass(field.type_, date): - return Date - if issubclass(field.type_, timedelta): - return Interval - if issubclass(field.type_, time): - return Time - if issubclass(field.type_, bytes): - return LargeBinary - if issubclass(field.type_, Decimal): - return Numeric( - precision=getattr(field.type_, "max_digits", None), - scale=getattr(field.type_, "decimal_places", None), - ) - if issubclass(field.type_, ipaddress.IPv4Address): - return AutoString - if issubclass(field.type_, ipaddress.IPv4Network): - return AutoString - if issubclass(field.type_, ipaddress.IPv6Address): - return AutoString - if issubclass(field.type_, ipaddress.IPv6Network): - return AutoString - if issubclass(field.type_, Path): - return AutoString - if issubclass(field.type_, uuid.UUID): - return GUID - raise ValueError(f"The field {field.name} has no matching SQLAlchemy type") - - -def get_column_from_field(field: ModelField) -> Column: # type: ignore - sa_column = getattr(field.field_info, "sa_column", Undefined) + + type_ = get_type_from_field(field) + metadata = get_field_metadata(field) + + # Check enums first as an enum can also be a str, needed by Pydantic/FastAPI + if issubclass(type_, Enum): + return sa_Enum(type_) + if issubclass(type_, str): + max_length = getattr(metadata, "max_length", None) + if max_length: + return AutoString(length=max_length) + return AutoString + if issubclass(type_, float): + return Float + if issubclass(type_, bool): + return Boolean + if issubclass(type_, int): + return Integer + if issubclass(type_, datetime): + return DateTime + if issubclass(type_, date): + return Date + if issubclass(type_, timedelta): + return Interval + if issubclass(type_, time): + return Time + if issubclass(type_, bytes): + return LargeBinary + if issubclass(type_, Decimal): + return Numeric( + precision=getattr(metadata, "max_digits", None), + scale=getattr(metadata, "decimal_places", None), + ) + if issubclass(type_, ipaddress.IPv4Address): + return AutoString + if issubclass(type_, ipaddress.IPv4Network): + return AutoString + if issubclass(type_, ipaddress.IPv6Address): + return AutoString + if issubclass(type_, ipaddress.IPv6Network): + return AutoString + if issubclass(type_, Path): + return AutoString + if issubclass(type_, uuid.UUID): + return GUID + raise ValueError(f"{type_} has no matching SQLAlchemy type") + + +def get_column_from_field(field: Any) -> Column: # type: ignore + if IS_PYDANTIC_V2: + field_info = field + else: + field_info = field.field_info + sa_column = getattr(field_info, "sa_column", Undefined) if isinstance(sa_column, Column): return sa_column sa_type = get_sqlalchemy_type(field) - primary_key = getattr(field.field_info, "primary_key", Undefined) + primary_key = getattr(field_info, "primary_key", Undefined) if primary_key is Undefined: primary_key = False - index = getattr(field.field_info, "index", Undefined) + index = getattr(field_info, "index", Undefined) if index is Undefined: index = False - nullable = not primary_key and _is_field_noneable(field) + nullable = not primary_key and is_field_noneable(field) # Override derived nullability if the nullable property is set explicitly # on the field - field_nullable = getattr(field.field_info, "nullable", Undefined) # noqa: B009 - if field_nullable != Undefined: + field_nullable = getattr(field_info, "nullable", Undefined) # noqa: B009 + if field_nullable is not Undefined: assert not isinstance(field_nullable, UndefinedType) nullable = field_nullable args = [] - foreign_key = getattr(field.field_info, "foreign_key", Undefined) + foreign_key = getattr(field_info, "foreign_key", Undefined) if foreign_key is Undefined: foreign_key = None - unique = getattr(field.field_info, "unique", Undefined) + unique = getattr(field_info, "unique", Undefined) if unique is Undefined: unique = False if foreign_key: @@ -620,16 +646,16 @@ def get_column_from_field(field: ModelField) -> Column: # type: ignore "unique": unique, } sa_default = Undefined - if field.field_info.default_factory: - sa_default = field.field_info.default_factory - elif field.field_info.default is not Undefined: - sa_default = field.field_info.default + if field_info.default_factory: + sa_default = field_info.default_factory + elif field_info.default is not Undefined: + sa_default = field_info.default if sa_default is not Undefined: kwargs["default"] = sa_default - sa_column_args = getattr(field.field_info, "sa_column_args", Undefined) + sa_column_args = getattr(field_info, "sa_column_args", Undefined) if sa_column_args is not Undefined: args.extend(list(cast(Sequence[Any], sa_column_args))) - sa_column_kwargs = getattr(field.field_info, "sa_column_kwargs", Undefined) + sa_column_kwargs = getattr(field_info, "sa_column_kwargs", Undefined) if sa_column_kwargs is not Undefined: kwargs.update(cast(Dict[Any, Any], sa_column_kwargs)) return Column(sa_type, *args, **kwargs) # type: ignore @@ -639,13 +665,6 @@ def get_column_from_field(field: ModelField) -> Column: # type: ignore default_registry = registry() - -def _value_items_is_true(v: Any) -> bool: - # Re-implement Pydantic's ValueItems.is_true() as it hasn't been released as of - # the current latest, Pydantic 1.8.2 - return v is True or v is ... - - _TSQLModel = TypeVar("_TSQLModel", bound="SQLModel") @@ -653,13 +672,17 @@ class SQLModel(BaseModel, metaclass=SQLModelMetaclass, registry=default_registry # SQLAlchemy needs to set weakref(s), Pydantic will set the other slots values __slots__ = ("__weakref__",) __tablename__: ClassVar[Union[str, Callable[..., str]]] - __sqlmodel_relationships__: ClassVar[Dict[str, RelationshipProperty]] # type: ignore + __sqlmodel_relationships__: ClassVar[Dict[str, RelationshipProperty[Any]]] __name__: ClassVar[str] metadata: ClassVar[MetaData] __allow_unmapped__ = True # https://docs.sqlalchemy.org/en/20/changelog/migration_20.html#migration-20-step-six - class Config: - orm_mode = True + if IS_PYDANTIC_V2: + model_config = SQLModelConfig(from_attributes=True) + else: + + class Config: + orm_mode = True def __new__(cls, *args: Any, **kwargs: Any) -> Any: new_object = super().__new__(cls) @@ -668,31 +691,28 @@ def __new__(cls, *args: Any, **kwargs: Any) -> Any: # Set __fields_set__ here, that would have been set when calling __init__ # in the Pydantic model so that when SQLAlchemy sets attributes that are # added (e.g. when querying from DB) to the __fields_set__, this already exists - object.__setattr__(new_object, "__fields_set__", set()) + set_fields_set(new_object, set()) return new_object def __init__(__pydantic_self__, **data: Any) -> None: # Uses something other than `self` the first arg to allow "self" as a # settable attribute - values, fields_set, validation_error = validate_model( - __pydantic_self__.__class__, data - ) - # Only raise errors if not a SQLModel model - if ( - not getattr(__pydantic_self__.__config__, "table", False) - and validation_error - ): - raise validation_error - # Do not set values as in Pydantic, pass them through setattr, so SQLAlchemy - # can handle them - # object.__setattr__(__pydantic_self__, '__dict__', values) - for key, value in values.items(): - setattr(__pydantic_self__, key, value) - object.__setattr__(__pydantic_self__, "__fields_set__", fields_set) - non_pydantic_keys = data.keys() - values.keys() - for key in non_pydantic_keys: - if key in __pydantic_self__.__sqlmodel_relationships__: - setattr(__pydantic_self__, key, data[key]) + + # SQLAlchemy does very dark black magic and modifies the __init__ method in + # sqlalchemy.orm.instrumentation._generate_init() + # so, to make SQLAlchemy work, it's needed to explicitly call __init__ to + # trigger all the SQLAlchemy logic, it doesn't work using cls.__new__, setting + # attributes obj.__dict__, etc. The __init__ method has to be called. But + # there are cases where calling all the default logic is not ideal, e.g. + # when calling Model.model_validate(), as the validation is done outside + # of instance creation. + # At the same time, __init__ is what users would normally call, by creating + # a new instance, which should have validation and all the default logic. + # So, to be able to set up the internal SQLAlchemy logic alone without + # executing the rest, and support things like Model.model_validate(), we + # use a contextvar to know if we should execute everything. + if finish_init.get(): + sqlmodel_init(self=__pydantic_self__, data=data) def __setattr__(self, name: str, value: Any) -> None: if name in {"_sa_instance_state"}: @@ -700,59 +720,13 @@ def __setattr__(self, name: str, value: Any) -> None: return else: # Set in SQLAlchemy, before Pydantic to trigger events and updates - if getattr(self.__config__, "table", False) and is_instrumented(self, name): # type: ignore + if is_table_model_class(self.__class__) and is_instrumented(self, name): # type: ignore[no-untyped-call] set_attribute(self, name, value) # Set in Pydantic model to trigger possible validation changes, only for # non relationship values if name not in self.__sqlmodel_relationships__: super().__setattr__(name, value) - @classmethod - def from_orm( - cls: Type[_TSQLModel], obj: Any, update: Optional[Dict[str, Any]] = None - ) -> _TSQLModel: - # Duplicated from Pydantic - if not cls.__config__.orm_mode: - raise ConfigError( - "You must have the config attribute orm_mode=True to use from_orm" - ) - obj = {ROOT_KEY: obj} if cls.__custom_root_type__ else cls._decompose_class(obj) - # SQLModel, support update dict - if update is not None: - obj = {**obj, **update} - # End SQLModel support dict - if not getattr(cls.__config__, "table", False): - # If not table, normal Pydantic code - m: _TSQLModel = cls.__new__(cls) - else: - # If table, create the new instance normally to make SQLAlchemy create - # the _sa_instance_state attribute - m = cls() - values, fields_set, validation_error = validate_model(cls, obj) - if validation_error: - raise validation_error - # Updated to trigger SQLAlchemy internal handling - if not getattr(cls.__config__, "table", False): - object.__setattr__(m, "__dict__", values) - else: - for key, value in values.items(): - setattr(m, key, value) - # Continue with standard Pydantic logic - object.__setattr__(m, "__fields_set__", fields_set) - m._init_private_attributes() - return m - - @classmethod - def parse_obj( - cls: Type[_TSQLModel], obj: Any, update: Optional[Dict[str, Any]] = None - ) -> _TSQLModel: - obj = cls._enforce_dict_if_root(obj) - # SQLModel, support update dict - if update is not None: - obj = {**obj, **update} - # End SQLModel support dict - return super().parse_obj(obj) - def __repr_args__(self) -> Sequence[Tuple[Optional[str], Any]]: # Don't show SQLAlchemy private attributes return [ @@ -761,33 +735,126 @@ def __repr_args__(self) -> Sequence[Tuple[Optional[str], Any]]: if not (isinstance(k, str) and k.startswith("_sa_")) ] - # From Pydantic, override to enforce validation with dict + @declared_attr # type: ignore + def __tablename__(cls) -> str: + return cls.__name__.lower() + @classmethod - def validate(cls: Type[_TSQLModel], value: Any) -> _TSQLModel: - if isinstance(value, cls): - return value.copy() if cls.__config__.copy_on_model_validation else value - - value = cls._enforce_dict_if_root(value) - if isinstance(value, dict): - values, fields_set, validation_error = validate_model(cls, value) - if validation_error: - raise validation_error - model = cls(**value) - # Reset fields set, this would have been done in Pydantic in __init__ - object.__setattr__(model, "__fields_set__", fields_set) - return model - elif cls.__config__.orm_mode: - return cls.from_orm(value) - elif cls.__custom_root_type__: - return cls.parse_obj(value) + def model_validate( + cls: Type[_TSQLModel], + obj: Any, + *, + strict: Union[bool, None] = None, + from_attributes: Union[bool, None] = None, + context: Union[Dict[str, Any], None] = None, + update: Union[Dict[str, Any], None] = None, + ) -> _TSQLModel: + return sqlmodel_validate( + cls=cls, + obj=obj, + strict=strict, + from_attributes=from_attributes, + context=context, + update=update, + ) + + # TODO: remove when deprecating Pydantic v1, only for compatibility + def model_dump( + self, + *, + mode: Union[Literal["json", "python"], str] = "python", + include: IncEx = None, + exclude: IncEx = None, + by_alias: bool = False, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + round_trip: bool = False, + warnings: bool = True, + ) -> Dict[str, Any]: + if IS_PYDANTIC_V2: + return super().model_dump( + mode=mode, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + round_trip=round_trip, + warnings=warnings, + ) else: - try: - value_as_dict = dict(value) - except (TypeError, ValueError) as e: - raise DictError() from e - return cls(**value_as_dict) + return super().dict( + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + + @deprecated( + """ + 🚨 `obj.dict()` was deprecated in SQLModel 0.0.14, you should + instead use `obj.model_dump()`. + """ + ) + def dict( + self, + *, + include: IncEx = None, + exclude: IncEx = None, + by_alias: bool = False, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + ) -> Dict[str, Any]: + return self.model_dump( + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + + @classmethod + @deprecated( + """ + 🚨 `obj.from_orm(data)` was deprecated in SQLModel 0.0.14, you should + instead use `obj.model_validate(data)`. + """ + ) + def from_orm( + cls: Type[_TSQLModel], obj: Any, update: Optional[Dict[str, Any]] = None + ) -> _TSQLModel: + return cls.model_validate(obj, update=update) + + @classmethod + @deprecated( + """ + 🚨 `obj.parse_obj(data)` was deprecated in SQLModel 0.0.14, you should + instead use `obj.model_validate(data)`. + """ + ) + def parse_obj( + cls: Type[_TSQLModel], obj: Any, update: Optional[Dict[str, Any]] = None + ) -> _TSQLModel: + if not IS_PYDANTIC_V2: + obj = cls._enforce_dict_if_root(obj) # type: ignore[attr-defined] # noqa + return cls.model_validate(obj, update=update) # From Pydantic, override to only show keys from fields, omit SQLAlchemy attributes + @deprecated( + """ + 🚨 You should not access `obj._calculate_keys()` directly. + + It is only useful for Pydantic v1.X, you should probably upgrade to + Pydantic v2.X. + """, + category=None, + ) def _calculate_keys( self, include: Optional[Mapping[Union[int, str], Any]], @@ -795,44 +862,10 @@ def _calculate_keys( exclude_unset: bool, update: Optional[Dict[str, Any]] = None, ) -> Optional[AbstractSet[str]]: - if include is None and exclude is None and not exclude_unset: - # Original in Pydantic: - # return None - # Updated to not return SQLAlchemy attributes - # Do not include relationships as that would easily lead to infinite - # recursion, or traversing the whole database - return self.__fields__.keys() # | self.__sqlmodel_relationships__.keys() - - keys: AbstractSet[str] - if exclude_unset: - keys = self.__fields_set__.copy() - else: - # Original in Pydantic: - # keys = self.__dict__.keys() - # Updated to not return SQLAlchemy attributes - # Do not include relationships as that would easily lead to infinite - # recursion, or traversing the whole database - keys = self.__fields__.keys() # | self.__sqlmodel_relationships__.keys() - if include is not None: - keys &= include.keys() - - if update: - keys -= update.keys() - - if exclude: - keys -= {k for k, v in exclude.items() if _value_items_is_true(v)} - - return keys - - @declared_attr # type: ignore - def __tablename__(cls) -> str: - return cls.__name__.lower() - - -def _is_field_noneable(field: ModelField) -> bool: - if not field.required: - # Taken from [Pydantic](https://github.com/samuelcolvin/pydantic/blob/v1.8.2/pydantic/fields.py#L946-L947) - return field.allow_none and ( - field.shape != SHAPE_SINGLETON or not field.sub_fields + return _calculate_keys( + self, + include=include, + exclude=exclude, + exclude_unset=exclude_unset, + update=update, ) - return False diff --git a/tests/conftest.py b/tests/conftest.py index 7b2cfcd6d5..e273e23538 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,7 @@ import pytest from pydantic import BaseModel from sqlmodel import SQLModel +from sqlmodel._compat import IS_PYDANTIC_V2 from sqlmodel.main import default_registry top_level_path = Path(__file__).resolve().parent.parent @@ -56,12 +57,12 @@ def new_print(*args): data = [] for arg in args: if isinstance(arg, BaseModel): - data.append(arg.dict()) + data.append(arg.model_dump()) elif isinstance(arg, list): new_list = [] for item in arg: if isinstance(item, BaseModel): - new_list.append(item.dict()) + new_list.append(item.model_dump()) data.append(new_list) else: data.append(arg) @@ -70,6 +71,9 @@ def new_print(*args): return new_print +needs_pydanticv2 = pytest.mark.skipif(not IS_PYDANTIC_V2, reason="requires Pydantic v2") +needs_pydanticv1 = pytest.mark.skipif(IS_PYDANTIC_V2, reason="requires Pydantic v1") + needs_py39 = pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python3.9+") needs_py310 = pytest.mark.skipif( sys.version_info < (3, 10), reason="requires python3.10+" diff --git a/tests/test_deprecations.py b/tests/test_deprecations.py new file mode 100644 index 0000000000..ef66c91b53 --- /dev/null +++ b/tests/test_deprecations.py @@ -0,0 +1,30 @@ +import pytest +from sqlmodel import SQLModel + + +class Item(SQLModel): + name: str + + +class SubItem(Item): + password: str + + +def test_deprecated_from_orm_inheritance(): + new_item = SubItem(name="Hello", password="secret") + with pytest.warns(DeprecationWarning): + item = Item.from_orm(new_item) + assert item.name == "Hello" + assert not hasattr(item, "password") + + +def test_deprecated_parse_obj(): + with pytest.warns(DeprecationWarning): + item = Item.parse_obj({"name": "Hello"}) + assert item.name == "Hello" + + +def test_deprecated_dict(): + with pytest.warns(DeprecationWarning): + data = Item(name="Hello").dict() + assert data == {"name": "Hello"} diff --git a/tests/test_enums.py b/tests/test_enums.py index 194bdefea1..f0543e90f1 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -5,6 +5,8 @@ from sqlalchemy.sql.type_api import TypeEngine from sqlmodel import Field, SQLModel +from .conftest import needs_pydanticv1, needs_pydanticv2 + """ Tests related to Enums @@ -72,7 +74,8 @@ def test_sqlite_ddl_sql(capsys): assert "CREATE TYPE" not in captured.out -def test_json_schema_flat_model(): +@needs_pydanticv1 +def test_json_schema_flat_model_pydantic_v1(): assert FlatModel.schema() == { "title": "FlatModel", "type": "object", @@ -92,7 +95,8 @@ def test_json_schema_flat_model(): } -def test_json_schema_inherit_model(): +@needs_pydanticv1 +def test_json_schema_inherit_model_pydantic_v1(): assert InheritModel.schema() == { "title": "InheritModel", "type": "object", @@ -110,3 +114,35 @@ def test_json_schema_inherit_model(): } }, } + + +@needs_pydanticv2 +def test_json_schema_flat_model_pydantic_v2(): + assert FlatModel.model_json_schema() == { + "title": "FlatModel", + "type": "object", + "properties": { + "id": {"title": "Id", "type": "string", "format": "uuid"}, + "enum_field": {"$ref": "#/$defs/MyEnum1"}, + }, + "required": ["id", "enum_field"], + "$defs": { + "MyEnum1": {"enum": ["A", "B"], "title": "MyEnum1", "type": "string"} + }, + } + + +@needs_pydanticv2 +def test_json_schema_inherit_model_pydantic_v2(): + assert InheritModel.model_json_schema() == { + "title": "InheritModel", + "type": "object", + "properties": { + "id": {"title": "Id", "type": "string", "format": "uuid"}, + "enum_field": {"$ref": "#/$defs/MyEnum2"}, + }, + "required": ["id", "enum_field"], + "$defs": { + "MyEnum2": {"enum": ["C", "D"], "title": "MyEnum2", "type": "string"} + }, + } diff --git a/tests/test_field_sa_relationship.py b/tests/test_field_sa_relationship.py index 7606fd86d8..022a100a78 100644 --- a/tests/test_field_sa_relationship.py +++ b/tests/test_field_sa_relationship.py @@ -6,7 +6,7 @@ def test_sa_relationship_no_args() -> None: - with pytest.raises(RuntimeError): + with pytest.raises(RuntimeError): # pragma: no cover class Team(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) @@ -30,7 +30,7 @@ class Hero(SQLModel, table=True): def test_sa_relationship_no_kwargs() -> None: - with pytest.raises(RuntimeError): + with pytest.raises(RuntimeError): # pragma: no cover class Team(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) diff --git a/tests/test_instance_no_args.py b/tests/test_instance_no_args.py index 14d560628b..5c8ad77531 100644 --- a/tests/test_instance_no_args.py +++ b/tests/test_instance_no_args.py @@ -1,19 +1,16 @@ from typing import Optional -from sqlalchemy import create_engine, select -from sqlalchemy.orm import Session -from sqlmodel import Field, SQLModel +import pytest +from pydantic import ValidationError +from sqlmodel import Field, Session, SQLModel, create_engine, select def test_allow_instantiation_without_arguments(clear_sqlmodel): - class Item(SQLModel): + class Item(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) name: str description: Optional[str] = None - class Config: - table = True - engine = create_engine("sqlite:///:memory:") SQLModel.metadata.create_all(engine) with Session(engine) as db: @@ -21,7 +18,18 @@ class Config: item.name = "Rick" db.add(item) db.commit() - result = db.execute(select(Item)).scalars().all() + statement = select(Item) + result = db.exec(statement).all() assert len(result) == 1 assert isinstance(item.id, int) SQLModel.metadata.clear() + + +def test_not_allow_instantiation_without_arguments_if_not_table(): + class Item(SQLModel): + id: Optional[int] = Field(default=None, primary_key=True) + name: str + description: Optional[str] = None + + with pytest.raises(ValidationError): + Item() diff --git a/tests/test_main.py b/tests/test_main.py index bdbcdeb76d..60d5c40ebb 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -91,7 +91,6 @@ class Hero(SQLModel, table=True): with Session(engine) as session: session.add(hero_2) session.commit() - session.refresh(hero_2) def test_sa_relationship_property(clear_sqlmodel): diff --git a/tests/test_missing_type.py b/tests/test_missing_type.py index 2185fa43e9..ac4aa42e05 100644 --- a/tests/test_missing_type.py +++ b/tests/test_missing_type.py @@ -1,17 +1,18 @@ from typing import Optional import pytest +from pydantic import BaseModel from sqlmodel import Field, SQLModel def test_missing_sql_type(): - class CustomType: + class CustomType(BaseModel): @classmethod def __get_validators__(cls): yield cls.validate @classmethod - def validate(cls, v): + def validate(cls, v): # pragma: no cover return v with pytest.raises(ValueError): diff --git a/tests/test_nullable.py b/tests/test_nullable.py index 1c8b37b218..a40bb5b5f0 100644 --- a/tests/test_nullable.py +++ b/tests/test_nullable.py @@ -58,7 +58,7 @@ class Hero(SQLModel, table=True): ][0] assert "primary_key INTEGER NOT NULL," in create_table_log assert "required_value VARCHAR NOT NULL," in create_table_log - assert "optional_default_ellipsis VARCHAR NOT NULL," in create_table_log + assert "optional_default_ellipsis VARCHAR," in create_table_log assert "optional_default_none VARCHAR," in create_table_log assert "optional_non_nullable VARCHAR NOT NULL," in create_table_log assert "optional_nullable VARCHAR," in create_table_log diff --git a/tests/test_query.py b/tests/test_query.py index abca97253b..88517b92fe 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -1,5 +1,6 @@ from typing import Optional +import pytest from sqlmodel import Field, Session, SQLModel, create_engine @@ -21,6 +22,7 @@ class Hero(SQLModel, table=True): session.refresh(hero_1) with Session(engine) as session: - query_hero = session.query(Hero).first() + with pytest.warns(DeprecationWarning): + query_hero = session.query(Hero).first() assert query_hero assert query_hero.name == hero_1.name diff --git a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py index 6a55d6cb98..706cc8aed7 100644 --- a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -284,7 +285,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "HeroRead": { @@ -294,7 +304,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), "id": {"title": "Id", "type": "integer"}, }, }, @@ -302,9 +321,36 @@ def test_tutorial(clear_sqlmodel): "title": "HeroUpdate", "type": "object", "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "name": IsDict( + { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ), + "secret_name": IsDict( + { + "title": "Secret Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Secret Name", "type": "string"} + ), + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py310.py index 133b287630..46c8c42dd3 100644 --- a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py310.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -287,7 +288,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "HeroRead": { @@ -297,7 +307,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), "id": {"title": "Id", "type": "integer"}, }, }, @@ -305,9 +324,36 @@ def test_tutorial(clear_sqlmodel): "title": "HeroUpdate", "type": "object", "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "name": IsDict( + { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ), + "secret_name": IsDict( + { + "title": "Secret Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Secret Name", "type": "string"} + ), + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py39.py index 5aac8cb11f..e2874c1095 100644 --- a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py39.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -287,7 +288,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "HeroRead": { @@ -297,7 +307,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), "id": {"title": "Id", "type": "integer"}, }, }, @@ -305,9 +324,36 @@ def test_tutorial(clear_sqlmodel): "title": "HeroUpdate", "type": "object", "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "name": IsDict( + { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ), + "secret_name": IsDict( + { + "title": "Secret Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Secret Name", "type": "string"} + ), + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py index 2709231504..d177c80c4c 100644 --- a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -217,7 +218,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "HeroRead": { @@ -227,7 +237,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), "id": {"title": "Id", "type": "integer"}, }, }, diff --git a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py310.py index ee0d89ac55..03086996ca 100644 --- a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py310.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -220,7 +221,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "HeroRead": { @@ -230,7 +240,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), "id": {"title": "Id", "type": "integer"}, }, }, diff --git a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py39.py index f4ef44abc5..f7e42e4e20 100644 --- a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py39.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -220,7 +221,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "HeroRead": { @@ -230,7 +240,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), "id": {"title": "Id", "type": "integer"}, }, }, diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py index 7444f8858d..2ebfc0c0d0 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlalchemy import inspect from sqlalchemy.engine.reflection import Inspector @@ -53,11 +54,10 @@ def test_tutorial(clear_sqlmodel): assert data[1]["id"] != hero2_data["id"] response = client.get("/openapi.json") - data = response.json() assert response.status_code == 200, response.text - assert data == { + assert response.json() == { "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { @@ -142,7 +142,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "HeroRead": { @@ -153,7 +162,16 @@ def test_tutorial(clear_sqlmodel): "id": {"title": "Id", "type": "integer"}, "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py310.py index 080a907e0e..c17e482921 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py310.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlalchemy import inspect from sqlalchemy.engine.reflection import Inspector @@ -56,11 +57,9 @@ def test_tutorial(clear_sqlmodel): assert data[1]["id"] != hero2_data["id"] response = client.get("/openapi.json") - data = response.json() - assert response.status_code == 200, response.text - assert data == { + assert response.json() == { "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { @@ -145,7 +144,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "HeroRead": { @@ -156,7 +164,16 @@ def test_tutorial(clear_sqlmodel): "id": {"title": "Id", "type": "integer"}, "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py39.py index 7c320093ae..258b3a4e54 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py39.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlalchemy import inspect from sqlalchemy.engine.reflection import Inspector @@ -56,11 +57,10 @@ def test_tutorial(clear_sqlmodel): assert data[1]["id"] != hero2_data["id"] response = client.get("/openapi.json") - data = response.json() assert response.status_code == 200, response.text - assert data == { + assert response.json() == { "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { @@ -145,7 +145,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "HeroRead": { @@ -156,7 +165,16 @@ def test_tutorial(clear_sqlmodel): "id": {"title": "Id", "type": "integer"}, "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py index 4a6bb7499e..47f2e64155 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlalchemy import inspect from sqlalchemy.engine.reflection import Inspector @@ -53,11 +54,10 @@ def test_tutorial(clear_sqlmodel): assert data[1]["id"] != hero2_data["id"] response = client.get("/openapi.json") - data = response.json() assert response.status_code == 200, response.text - assert data == { + assert response.json() == { "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { @@ -142,7 +142,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "HeroRead": { @@ -152,7 +161,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), "id": {"title": "Id", "type": "integer"}, }, }, diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py310.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py310.py index 20195c6fdf..c09b15bd53 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py310.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py310.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlalchemy import inspect from sqlalchemy.engine.reflection import Inspector @@ -56,11 +57,10 @@ def test_tutorial(clear_sqlmodel): assert data[1]["id"] != hero2_data["id"] response = client.get("/openapi.json") - data = response.json() assert response.status_code == 200, response.text - assert data == { + assert response.json() == { "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { @@ -145,7 +145,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "HeroRead": { @@ -155,7 +164,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), "id": {"title": "Id", "type": "integer"}, }, }, diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py39.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py39.py index 45b061b401..8ad0f271e1 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py39.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py39.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlalchemy import inspect from sqlalchemy.engine.reflection import Inspector @@ -56,11 +57,10 @@ def test_tutorial(clear_sqlmodel): assert data[1]["id"] != hero2_data["id"] response = client.get("/openapi.json") - data = response.json() assert response.status_code == 200, response.text - assert data == { + assert response.json() == { "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { @@ -145,7 +145,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "HeroRead": { @@ -155,7 +164,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), "id": {"title": "Id", "type": "integer"}, }, }, diff --git a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py index 5d2327095e..62fbb25a9c 100644 --- a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -38,11 +39,10 @@ def test_tutorial(clear_sqlmodel): assert response.status_code == 404, response.text response = client.get("/openapi.json") - data = response.json() assert response.status_code == 200, response.text - assert data == { + assert response.json() == { "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { @@ -163,7 +163,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "HeroRead": { @@ -173,7 +182,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), "id": {"title": "Id", "type": "integer"}, }, }, diff --git a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py310.py index 2e0a97e780..913d098882 100644 --- a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py310.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -41,11 +42,10 @@ def test_tutorial(clear_sqlmodel): assert response.status_code == 404, response.text response = client.get("/openapi.json") - data = response.json() assert response.status_code == 200, response.text - assert data == { + assert response.json() == { "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { @@ -166,7 +166,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "HeroRead": { @@ -176,7 +185,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), "id": {"title": "Id", "type": "integer"}, }, }, diff --git a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py39.py index a663eccac3..9bedf5c62d 100644 --- a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py39.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -41,11 +42,10 @@ def test_tutorial(clear_sqlmodel): assert response.status_code == 404, response.text response = client.get("/openapi.json") - data = response.json() assert response.status_code == 200, response.text - assert data == { + assert response.json() == { "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { @@ -166,7 +166,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "HeroRead": { @@ -176,7 +185,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), "id": {"title": "Id", "type": "integer"}, }, }, diff --git a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py index 2c60ce6d04..b301697dbf 100644 --- a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -531,8 +532,26 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "team_id": {"title": "Team Id", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), + "team_id": IsDict( + { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Team Id", "type": "integer"} + ), }, }, "HeroRead": { @@ -542,8 +561,26 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "team_id": {"title": "Team Id", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), + "team_id": IsDict( + { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Team Id", "type": "integer"} + ), "id": {"title": "Id", "type": "integer"}, }, }, @@ -554,20 +591,85 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "team_id": {"title": "Team Id", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), + "team_id": IsDict( + { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Team Id", "type": "integer"} + ), "id": {"title": "Id", "type": "integer"}, - "team": {"$ref": "#/components/schemas/TeamRead"}, + "team": IsDict( + { + "anyOf": [ + {"$ref": "#/components/schemas/TeamRead"}, + {"type": "null"}, + ] + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"$ref": "#/components/schemas/TeamRead"} + ), }, }, "HeroUpdate": { "title": "HeroUpdate", "type": "object", "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "team_id": {"title": "Team Id", "type": "integer"}, + "name": IsDict( + { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ), + "secret_name": IsDict( + { + "title": "Secret Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Secret Name", "type": "string"} + ), + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), + "team_id": IsDict( + { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Team Id", "type": "integer"} + ), }, }, "TeamCreate": { @@ -609,9 +711,36 @@ def test_tutorial(clear_sqlmodel): "title": "TeamUpdate", "type": "object", "properties": { - "id": {"title": "Id", "type": "integer"}, - "name": {"title": "Name", "type": "string"}, - "headquarters": {"title": "Headquarters", "type": "string"}, + "id": IsDict( + { + "title": "Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Id", "type": "integer"} + ), + "name": IsDict( + { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ), + "headquarters": IsDict( + { + "title": "Headquarters", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Headquarters", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py310.py index 045a66ba5d..4d310a87e5 100644 --- a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py310.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -534,8 +535,26 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "team_id": {"title": "Team Id", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), + "team_id": IsDict( + { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Team Id", "type": "integer"} + ), }, }, "HeroRead": { @@ -545,8 +564,26 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "team_id": {"title": "Team Id", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), + "team_id": IsDict( + { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Team Id", "type": "integer"} + ), "id": {"title": "Id", "type": "integer"}, }, }, @@ -557,20 +594,85 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "team_id": {"title": "Team Id", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), + "team_id": IsDict( + { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Team Id", "type": "integer"} + ), "id": {"title": "Id", "type": "integer"}, - "team": {"$ref": "#/components/schemas/TeamRead"}, + "team": IsDict( + { + "anyOf": [ + {"$ref": "#/components/schemas/TeamRead"}, + {"type": "null"}, + ] + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"$ref": "#/components/schemas/TeamRead"} + ), }, }, "HeroUpdate": { "title": "HeroUpdate", "type": "object", "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "team_id": {"title": "Team Id", "type": "integer"}, + "name": IsDict( + { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ), + "secret_name": IsDict( + { + "title": "Secret Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Secret Name", "type": "string"} + ), + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), + "team_id": IsDict( + { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Team Id", "type": "integer"} + ), }, }, "TeamCreate": { @@ -612,9 +714,36 @@ def test_tutorial(clear_sqlmodel): "title": "TeamUpdate", "type": "object", "properties": { - "id": {"title": "Id", "type": "integer"}, - "name": {"title": "Name", "type": "string"}, - "headquarters": {"title": "Headquarters", "type": "string"}, + "id": IsDict( + { + "title": "Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Id", "type": "integer"} + ), + "name": IsDict( + { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ), + "headquarters": IsDict( + { + "title": "Headquarters", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Headquarters", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py39.py index 924d0b90af..0603739c41 100644 --- a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py39.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -534,8 +535,26 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "team_id": {"title": "Team Id", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), + "team_id": IsDict( + { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Team Id", "type": "integer"} + ), }, }, "HeroRead": { @@ -545,8 +564,26 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "team_id": {"title": "Team Id", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), + "team_id": IsDict( + { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Team Id", "type": "integer"} + ), "id": {"title": "Id", "type": "integer"}, }, }, @@ -557,20 +594,85 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "team_id": {"title": "Team Id", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), + "team_id": IsDict( + { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Team Id", "type": "integer"} + ), "id": {"title": "Id", "type": "integer"}, - "team": {"$ref": "#/components/schemas/TeamRead"}, + "team": IsDict( + { + "anyOf": [ + {"$ref": "#/components/schemas/TeamRead"}, + {"type": "null"}, + ] + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"$ref": "#/components/schemas/TeamRead"} + ), }, }, "HeroUpdate": { "title": "HeroUpdate", "type": "object", "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "team_id": {"title": "Team Id", "type": "integer"}, + "name": IsDict( + { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ), + "secret_name": IsDict( + { + "title": "Secret Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Secret Name", "type": "string"} + ), + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), + "team_id": IsDict( + { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Team Id", "type": "integer"} + ), }, }, "TeamCreate": { @@ -612,9 +714,36 @@ def test_tutorial(clear_sqlmodel): "title": "TeamUpdate", "type": "object", "properties": { - "id": {"title": "Id", "type": "integer"}, - "name": {"title": "Name", "type": "string"}, - "headquarters": {"title": "Headquarters", "type": "string"}, + "id": IsDict( + { + "title": "Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Id", "type": "integer"} + ), + "name": IsDict( + { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ), + "headquarters": IsDict( + { + "title": "Headquarters", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Headquarters", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py index ca8a41845e..8f273bbd93 100644 --- a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -31,11 +32,10 @@ def test_tutorial(clear_sqlmodel): assert data[0]["secret_name"] == hero_data["secret_name"] response = client.get("/openapi.json") - data = response.json() assert response.status_code == 200, response.text - assert data == { + assert response.json() == { "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { @@ -114,10 +114,28 @@ def test_tutorial(clear_sqlmodel): "required": ["name", "secret_name"], "type": "object", "properties": { - "id": {"title": "Id", "type": "integer"}, + "id": IsDict( + { + "title": "Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Id", "type": "integer"} + ), "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py310.py index 4acb0068a1..d249cc4e90 100644 --- a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py310.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -34,11 +35,10 @@ def test_tutorial(clear_sqlmodel): assert data[0]["secret_name"] == hero_data["secret_name"] response = client.get("/openapi.json") - data = response.json() assert response.status_code == 200, response.text - assert data == { + assert response.json() == { "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { @@ -117,10 +117,28 @@ def test_tutorial(clear_sqlmodel): "required": ["name", "secret_name"], "type": "object", "properties": { - "id": {"title": "Id", "type": "integer"}, + "id": IsDict( + { + "title": "Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Id", "type": "integer"} + ), "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py39.py index 20f3f52313..b9fb2be03f 100644 --- a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py39.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -34,11 +35,10 @@ def test_tutorial(clear_sqlmodel): assert data[0]["secret_name"] == hero_data["secret_name"] response = client.get("/openapi.json") - data = response.json() assert response.status_code == 200, response.text - assert data == { + assert response.json() == { "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { @@ -117,10 +117,28 @@ def test_tutorial(clear_sqlmodel): "required": ["name", "secret_name"], "type": "object", "properties": { - "id": {"title": "Id", "type": "integer"}, + "id": IsDict( + { + "title": "Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Id", "type": "integer"} + ), "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py index 6f97cbf92b..441cc42b28 100644 --- a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -284,7 +285,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "HeroRead": { @@ -294,7 +304,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), "id": {"title": "Id", "type": "integer"}, }, }, @@ -302,9 +321,36 @@ def test_tutorial(clear_sqlmodel): "title": "HeroUpdate", "type": "object", "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "name": IsDict( + { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ), + "secret_name": IsDict( + { + "title": "Secret Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Secret Name", "type": "string"} + ), + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py310.py index f0c5416bdf..7c427a1c67 100644 --- a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py310.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -289,7 +290,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "HeroRead": { @@ -299,7 +309,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), "id": {"title": "Id", "type": "integer"}, }, }, @@ -307,9 +326,36 @@ def test_tutorial(clear_sqlmodel): "title": "HeroUpdate", "type": "object", "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "name": IsDict( + { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ), + "secret_name": IsDict( + { + "title": "Secret Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Secret Name", "type": "string"} + ), + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py39.py index 5b911c8462..ea63f52c41 100644 --- a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py39.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -289,7 +290,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "HeroRead": { @@ -299,7 +309,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), "id": {"title": "Id", "type": "integer"}, }, }, @@ -307,9 +326,36 @@ def test_tutorial(clear_sqlmodel): "title": "HeroUpdate", "type": "object", "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "name": IsDict( + { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ), + "secret_name": IsDict( + { + "title": "Secret Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Secret Name", "type": "string"} + ), + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py index 2136ed8a1f..9df7e50b81 100644 --- a/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -51,11 +52,10 @@ def test_tutorial(clear_sqlmodel): assert data[1]["id"] == hero2_data["id"] response = client.get("/openapi.json") - data = response.json() assert response.status_code == 200, response.text - assert data == { + assert response.json() == { "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { @@ -120,10 +120,28 @@ def test_tutorial(clear_sqlmodel): "required": ["name", "secret_name"], "type": "object", "properties": { - "id": {"title": "Id", "type": "integer"}, + "id": IsDict( + { + "title": "Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Id", "type": "integer"} + ), "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001_py310.py index d85d9ee5b2..a47513dde2 100644 --- a/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001_py310.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -54,11 +55,10 @@ def test_tutorial(clear_sqlmodel): assert data[1]["id"] == hero2_data["id"] response = client.get("/openapi.json") - data = response.json() assert response.status_code == 200, response.text - assert data == { + assert response.json() == { "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { @@ -123,10 +123,28 @@ def test_tutorial(clear_sqlmodel): "required": ["name", "secret_name"], "type": "object", "properties": { - "id": {"title": "Id", "type": "integer"}, + "id": IsDict( + { + "title": "Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Id", "type": "integer"} + ), "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py index a1be7b094a..a532625d42 100644 --- a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -518,8 +519,26 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "team_id": {"title": "Team Id", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), + "team_id": IsDict( + { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Team Id", "type": "integer"} + ), }, }, "HeroRead": { @@ -529,8 +548,26 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "team_id": {"title": "Team Id", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), + "team_id": IsDict( + { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Team Id", "type": "integer"} + ), "id": {"title": "Id", "type": "integer"}, }, }, @@ -538,10 +575,46 @@ def test_tutorial(clear_sqlmodel): "title": "HeroUpdate", "type": "object", "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "team_id": {"title": "Team Id", "type": "integer"}, + "name": IsDict( + { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ), + "secret_name": IsDict( + { + "title": "Secret Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Secret Name", "type": "string"} + ), + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), + "team_id": IsDict( + { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Team Id", "type": "integer"} + ), }, }, "TeamCreate": { @@ -567,8 +640,26 @@ def test_tutorial(clear_sqlmodel): "title": "TeamUpdate", "type": "object", "properties": { - "name": {"title": "Name", "type": "string"}, - "headquarters": {"title": "Headquarters", "type": "string"}, + "name": IsDict( + { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ), + "headquarters": IsDict( + { + "title": "Headquarters", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Headquarters", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py310.py index 882fcc796b..33029f6b60 100644 --- a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py310.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -521,8 +522,26 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "team_id": {"title": "Team Id", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), + "team_id": IsDict( + { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Team Id", "type": "integer"} + ), }, }, "HeroRead": { @@ -532,8 +551,26 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "team_id": {"title": "Team Id", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), + "team_id": IsDict( + { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Team Id", "type": "integer"} + ), "id": {"title": "Id", "type": "integer"}, }, }, @@ -541,10 +578,46 @@ def test_tutorial(clear_sqlmodel): "title": "HeroUpdate", "type": "object", "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "team_id": {"title": "Team Id", "type": "integer"}, + "name": IsDict( + { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ), + "secret_name": IsDict( + { + "title": "Secret Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Secret Name", "type": "string"} + ), + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), + "team_id": IsDict( + { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Team Id", "type": "integer"} + ), }, }, "TeamCreate": { @@ -570,8 +643,26 @@ def test_tutorial(clear_sqlmodel): "title": "TeamUpdate", "type": "object", "properties": { - "name": {"title": "Name", "type": "string"}, - "headquarters": {"title": "Headquarters", "type": "string"}, + "name": IsDict( + { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ), + "headquarters": IsDict( + { + "title": "Headquarters", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Headquarters", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py39.py index 12791b269c..66705e17cc 100644 --- a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py39.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -521,8 +522,26 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "team_id": {"title": "Team Id", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), + "team_id": IsDict( + { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Team Id", "type": "integer"} + ), }, }, "HeroRead": { @@ -532,8 +551,26 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "team_id": {"title": "Team Id", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), + "team_id": IsDict( + { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Team Id", "type": "integer"} + ), "id": {"title": "Id", "type": "integer"}, }, }, @@ -541,10 +578,46 @@ def test_tutorial(clear_sqlmodel): "title": "HeroUpdate", "type": "object", "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, - "team_id": {"title": "Team Id", "type": "integer"}, + "name": IsDict( + { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ), + "secret_name": IsDict( + { + "title": "Secret Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Secret Name", "type": "string"} + ), + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), + "team_id": IsDict( + { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Team Id", "type": "integer"} + ), }, }, "TeamCreate": { @@ -570,8 +643,26 @@ def test_tutorial(clear_sqlmodel): "title": "TeamUpdate", "type": "object", "properties": { - "name": {"title": "Name", "type": "string"}, - "headquarters": {"title": "Headquarters", "type": "string"}, + "name": IsDict( + { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ), + "headquarters": IsDict( + { + "title": "Headquarters", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Headquarters", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py index a4573ef11b..973ab2db04 100644 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -263,7 +264,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "HeroRead": { @@ -273,7 +283,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), "id": {"title": "Id", "type": "integer"}, }, }, @@ -281,9 +300,36 @@ def test_tutorial(clear_sqlmodel): "title": "HeroUpdate", "type": "object", "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "name": IsDict( + { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ), + "secret_name": IsDict( + { + "title": "Secret Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Secret Name", "type": "string"} + ), + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py310.py index cf56e3cb01..090af8c60f 100644 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py310.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -266,7 +267,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "HeroRead": { @@ -276,7 +286,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), "id": {"title": "Id", "type": "integer"}, }, }, @@ -284,9 +303,36 @@ def test_tutorial(clear_sqlmodel): "title": "HeroUpdate", "type": "object", "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "name": IsDict( + { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ), + "secret_name": IsDict( + { + "title": "Secret Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Secret Name", "type": "string"} + ), + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py39.py index b301ca3bf1..22dfb8f268 100644 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py39.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -266,7 +267,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "HeroRead": { @@ -276,7 +286,16 @@ def test_tutorial(clear_sqlmodel): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), "id": {"title": "Id", "type": "integer"}, }, }, @@ -284,9 +303,36 @@ def test_tutorial(clear_sqlmodel): "title": "HeroUpdate", "type": "object", "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": {"title": "Age", "type": "integer"}, + "name": IsDict( + { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ), + "secret_name": IsDict( + { + "title": "Secret Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Secret Name", "type": "string"} + ), + "age": IsDict( + { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), }, }, "ValidationError": { diff --git a/tests/test_validation.py b/tests/test_validation.py index ad60fcb945..3265922070 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -1,12 +1,14 @@ from typing import Optional import pytest -from pydantic import validator from pydantic.error_wrappers import ValidationError from sqlmodel import SQLModel +from .conftest import needs_pydanticv1, needs_pydanticv2 -def test_validation(clear_sqlmodel): + +@needs_pydanticv1 +def test_validation_pydantic_v1(clear_sqlmodel): """Test validation of implicit and explicit None values. # For consistency with pydantic, validators are not to be called on @@ -16,6 +18,7 @@ def test_validation(clear_sqlmodel): https://github.com/samuelcolvin/pydantic/issues/1223 """ + from pydantic import validator class Hero(SQLModel): name: Optional[str] = None @@ -31,3 +34,32 @@ def reject_none(cls, v): with pytest.raises(ValidationError): Hero.validate({"name": None, "age": 25}) + + +@needs_pydanticv2 +def test_validation_pydantic_v2(clear_sqlmodel): + """Test validation of implicit and explicit None values. + + # For consistency with pydantic, validators are not to be called on + # arguments that are not explicitly provided. + + https://github.com/tiangolo/sqlmodel/issues/230 + https://github.com/samuelcolvin/pydantic/issues/1223 + + """ + from pydantic import field_validator + + class Hero(SQLModel): + name: Optional[str] = None + secret_name: Optional[str] = None + age: Optional[int] = None + + @field_validator("name", "secret_name", "age") + def reject_none(cls, v): + assert v is not None + return v + + Hero.model_validate({"age": 25}) + + with pytest.raises(ValidationError): + Hero.model_validate({"name": None, "age": 25}) From bd24013a26e2d15311a18632ee845381ac31d899 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 4 Dec 2023 14:42:57 +0000 Subject: [PATCH 325/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index dcdd700717..62806885be 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Features + +* ✨ Add support for Pydantic v2 (while keeping support for v1 if v2 is not available), including initial work by AntonDeMeester. PR [#722](https://github.com/tiangolo/sqlmodel/pull/722) by [@tiangolo](https://github.com/tiangolo). + ## 0.0.13 ### Fixes From b892504141c842975b7a30156ab21755719e1d31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 4 Dec 2023 15:51:20 +0100 Subject: [PATCH 326/906] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.0.?= =?UTF-8?q?14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 4 +++- sqlmodel/__init__.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 62806885be..b2f290013e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,9 +2,11 @@ ## Latest Changes +## 0.0.14 + ### Features -* ✨ Add support for Pydantic v2 (while keeping support for v1 if v2 is not available), including initial work by AntonDeMeester. PR [#722](https://github.com/tiangolo/sqlmodel/pull/722) by [@tiangolo](https://github.com/tiangolo). +* ✨ Add support for Pydantic v2 (while keeping support for v1 if v2 is not available). PR [#722](https://github.com/tiangolo/sqlmodel/pull/722) by [@tiangolo](https://github.com/tiangolo) including initial work in PR [#699](https://github.com/tiangolo/sqlmodel/pull/699) by [@AntonDeMeester](https://github.com/AntonDeMeester). ## 0.0.13 diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index 0062dfaeb7..2df3afb51e 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.0.13" +__version__ = "0.0.14" # Re-export from SQLAlchemy from sqlalchemy.engine import create_engine as create_engine From 8419545a3af48b8e6390cb97b070039bb6d2eb14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 10 Dec 2023 20:19:12 +0000 Subject: [PATCH 327/906] =?UTF-8?q?=F0=9F=91=B7=20Fix=20GitHub=20Actions?= =?UTF-8?q?=20build=20docs=20filter=20paths=20for=20GitHub=20workflows=20(?= =?UTF-8?q?#738)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 👷 Fix GitHub Actions build docs filter paths for GitHub workflows * 🎨 Update format of expression and conftest --- .github/workflows/build-docs.yml | 4 ++-- sqlmodel/sql/expression.py | 2 +- sqlmodel/sql/expression.py.jinja2 | 2 +- tests/conftest.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 2e60ed7d39..e7de374652 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -30,8 +30,8 @@ jobs: - pyproject.toml - mkdocs.yml - mkdocs.insiders.yml - - ./github/workflows/build-docs.yml - - ./github/workflows/deploy-docs.yml + - .github/workflows/build-docs.yml + - .github/workflows/deploy-docs.yml build-docs: needs: diff --git a/sqlmodel/sql/expression.py b/sqlmodel/sql/expression.py index 2c931a1479..112968c655 100644 --- a/sqlmodel/sql/expression.py +++ b/sqlmodel/sql/expression.py @@ -153,7 +153,7 @@ def label( def nulls_first( - column: Union[_ColumnExpressionArgument[_T], _T] + column: Union[_ColumnExpressionArgument[_T], _T], ) -> UnaryExpression[_T]: return sqlalchemy.nulls_first(column) # type: ignore[arg-type] diff --git a/sqlmodel/sql/expression.py.jinja2 b/sqlmodel/sql/expression.py.jinja2 index 9bf93e1abf..53babe1bbe 100644 --- a/sqlmodel/sql/expression.py.jinja2 +++ b/sqlmodel/sql/expression.py.jinja2 @@ -151,7 +151,7 @@ def label( def nulls_first( - column: Union[_ColumnExpressionArgument[_T], _T] + column: Union[_ColumnExpressionArgument[_T], _T], ) -> UnaryExpression[_T]: return sqlalchemy.nulls_first(column) # type: ignore[arg-type] diff --git a/tests/conftest.py b/tests/conftest.py index e273e23538..a95eb3279f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -51,7 +51,7 @@ def coverage_run(*, module: str, cwd: Union[str, Path]) -> subprocess.CompletedP def get_testing_print_function( - calls: List[List[Union[str, Dict[str, Any]]]] + calls: List[List[Union[str, Dict[str, Any]]]], ) -> Callable[..., Any]: def new_print(*args): data = [] From fe497adf0c0793c9a88cf2cfef125b6bdd498851 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 10 Dec 2023 20:20:18 +0000 Subject: [PATCH 328/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index b2f290013e..4a8f4c9cdf 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Internal + +* 👷 Fix GitHub Actions build docs filter paths for GitHub workflows. PR [#738](https://github.com/tiangolo/sqlmodel/pull/738) by [@tiangolo](https://github.com/tiangolo). + ## 0.0.14 ### Features From 48b97f3d8ef0e5a177df8ad114d687e736ee52d1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jan 2024 07:49:37 +0100 Subject: [PATCH 329/906] =?UTF-8?q?=E2=AC=86=20Bump=20tiangolo/issue-manag?= =?UTF-8?q?er=20from=200.4.0=20to=200.4.1=20(#775)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [tiangolo/issue-manager](https://github.com/tiangolo/issue-manager) from 0.4.0 to 0.4.1. - [Release notes](https://github.com/tiangolo/issue-manager/releases) - [Commits](https://github.com/tiangolo/issue-manager/compare/0.4.0...0.4.1) --- updated-dependencies: - dependency-name: tiangolo/issue-manager dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/issue-manager.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml index e2fb4f7a43..4eeda34b45 100644 --- a/.github/workflows/issue-manager.yml +++ b/.github/workflows/issue-manager.yml @@ -18,7 +18,7 @@ jobs: issue-manager: runs-on: ubuntu-latest steps: - - uses: tiangolo/issue-manager@0.4.0 + - uses: tiangolo/issue-manager@0.4.1 with: token: ${{ secrets.GITHUB_TOKEN }} config: > From 0c7def88b5d9652bf9288738e2e9276d9dd24b5f Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 9 Jan 2024 06:52:30 +0000 Subject: [PATCH 330/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 4a8f4c9cdf..96c6feaf84 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Internal +* ⬆ Bump tiangolo/issue-manager from 0.4.0 to 0.4.1. PR [#775](https://github.com/tiangolo/sqlmodel/pull/775) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Fix GitHub Actions build docs filter paths for GitHub workflows. PR [#738](https://github.com/tiangolo/sqlmodel/pull/738) by [@tiangolo](https://github.com/tiangolo). ## 0.0.14 From 1b7b3aa668cf3cd70bbaa2024a7a2eb015af9bc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 17 Feb 2024 14:34:57 +0100 Subject: [PATCH 331/906] =?UTF-8?q?=F0=9F=90=9B=20Fix=20class=20initializa?= =?UTF-8?q?tion=20compatibility=20with=20Pydantic=20and=20SQLModel,=20fixi?= =?UTF-8?q?ng=20errors=20revealed=20by=20the=20latest=20Pydantic=20(#807)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 4 ++-- sqlmodel/_compat.py | 14 ++++++-------- sqlmodel/main.py | 6 +++--- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ade60f2559..89da640d15 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -62,10 +62,10 @@ jobs: run: python -m poetry install - name: Install Pydantic v1 if: matrix.pydantic-version == 'pydantic-v1' - run: pip install "pydantic>=1.10.0,<2.0.0" + run: pip install --upgrade "pydantic>=1.10.0,<2.0.0" - name: Install Pydantic v2 if: matrix.pydantic-version == 'pydantic-v2' - run: pip install "pydantic>=2.0.2,<3.0.0" + run: pip install --upgrade "pydantic>=2.0.2,<3.0.0" - name: Lint # Do not run on Python 3.7 as mypy behaves differently if: matrix.python-version != '3.7' && matrix.pydantic-version == 'pydantic-v2' diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index 2a2caca3e8..76771ce7ff 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -97,10 +97,10 @@ def set_config_value( def get_model_fields(model: InstanceOrType["SQLModel"]) -> Dict[str, "FieldInfo"]: return model.model_fields - def set_fields_set( - new_object: InstanceOrType["SQLModel"], fields: Set["FieldInfo"] - ) -> None: - object.__setattr__(new_object, "__pydantic_fields_set__", fields) + def init_pydantic_private_attrs(new_object: InstanceOrType["SQLModel"]) -> None: + object.__setattr__(new_object, "__pydantic_fields_set__", set()) + object.__setattr__(new_object, "__pydantic_extra__", None) + object.__setattr__(new_object, "__pydantic_private__", None) def get_annotations(class_dict: Dict[str, Any]) -> Dict[str, Any]: return class_dict.get("__annotations__", {}) @@ -387,10 +387,8 @@ def set_config_value( def get_model_fields(model: InstanceOrType["SQLModel"]) -> Dict[str, "FieldInfo"]: return model.__fields__ # type: ignore - def set_fields_set( - new_object: InstanceOrType["SQLModel"], fields: Set["FieldInfo"] - ) -> None: - object.__setattr__(new_object, "__fields_set__", fields) + def init_pydantic_private_attrs(new_object: InstanceOrType["SQLModel"]) -> None: + object.__setattr__(new_object, "__fields_set__", set()) def get_annotations(class_dict: Dict[str, Any]) -> Dict[str, Any]: return resolve_annotations( # type: ignore[no-any-return] diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 10064c7116..fec3bc7906 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -70,11 +70,11 @@ get_model_fields, get_relationship_to, get_type_from_field, + init_pydantic_private_attrs, is_field_noneable, is_table_model_class, post_init_field_info, set_config_value, - set_fields_set, sqlmodel_init, sqlmodel_validate, ) @@ -686,12 +686,12 @@ class Config: def __new__(cls, *args: Any, **kwargs: Any) -> Any: new_object = super().__new__(cls) - # SQLAlchemy doesn't call __init__ on the base class + # SQLAlchemy doesn't call __init__ on the base class when querying from DB # Ref: https://docs.sqlalchemy.org/en/14/orm/constructors.html # Set __fields_set__ here, that would have been set when calling __init__ # in the Pydantic model so that when SQLAlchemy sets attributes that are # added (e.g. when querying from DB) to the __fields_set__, this already exists - set_fields_set(new_object, set()) + init_pydantic_private_attrs(new_object) return new_object def __init__(__pydantic_self__, **data: Any) -> None: From d8fa5459557cab76f246d175f24f53a1ac75caf3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 17 Feb 2024 13:35:13 +0000 Subject: [PATCH 332/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 96c6feaf84..e37cbdadc6 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Fixes + +* 🐛 Fix class initialization compatibility with Pydantic and SQLModel, fixing errors revealed by the latest Pydantic. PR [#807](https://github.com/tiangolo/sqlmodel/pull/807) by [@tiangolo](https://github.com/tiangolo). + ### Internal * ⬆ Bump tiangolo/issue-manager from 0.4.0 to 0.4.1. PR [#775](https://github.com/tiangolo/sqlmodel/pull/775) by [@dependabot[bot]](https://github.com/apps/dependabot). From 7fec8848647fcb1a9853b014925af7b35a8e663a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 17 Feb 2024 14:36:12 +0100 Subject: [PATCH 333/906] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.0.?= =?UTF-8?q?15?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 2 ++ sqlmodel/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index e37cbdadc6..a0431a9bd7 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest Changes +## 0.0.15 + ### Fixes * 🐛 Fix class initialization compatibility with Pydantic and SQLModel, fixing errors revealed by the latest Pydantic. PR [#807](https://github.com/tiangolo/sqlmodel/pull/807) by [@tiangolo](https://github.com/tiangolo). diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index 2df3afb51e..c9629a98b1 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.0.14" +__version__ = "0.0.15" # Re-export from SQLAlchemy from sqlalchemy.engine import create_engine as create_engine From fa12c5d87b58d24017784af729b29e5b4141f2bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 17 Feb 2024 14:49:39 +0100 Subject: [PATCH 334/906] =?UTF-8?q?=E2=9C=A8=20Add=20new=20method=20`sqlmo?= =?UTF-8?q?del=5Fupdate()`=20to=20update=20models=20in=20place,=20includin?= =?UTF-8?q?g=20an=20`update`=20parameter=20for=20extra=20data=20(#804)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tutorial/fastapi/update-extra-data.md | 217 +++++++++ docs/tutorial/fastapi/update.md | 21 +- .../tutorial/fastapi/update/tutorial001.py | 3 +- .../fastapi/update/tutorial001_py310.py | 3 +- .../fastapi/update/tutorial001_py39.py | 3 +- .../tutorial/fastapi/update/tutorial002.py | 101 ++++ .../fastapi/update/tutorial002_py310.py | 99 ++++ .../fastapi/update/tutorial002_py39.py | 101 ++++ mkdocs.yml | 1 + sqlmodel/_compat.py | 24 +- sqlmodel/main.py | 30 +- tests/test_fields_set.py | 7 +- .../test_update/test_tutorial002.py | 427 +++++++++++++++++ .../test_update/test_tutorial002_py310.py | 430 ++++++++++++++++++ .../test_update/test_tutorial002_py39.py | 430 ++++++++++++++++++ 15 files changed, 1871 insertions(+), 26 deletions(-) create mode 100644 docs/tutorial/fastapi/update-extra-data.md create mode 100644 docs_src/tutorial/fastapi/update/tutorial002.py create mode 100644 docs_src/tutorial/fastapi/update/tutorial002_py310.py create mode 100644 docs_src/tutorial/fastapi/update/tutorial002_py39.py create mode 100644 tests/test_tutorial/test_fastapi/test_update/test_tutorial002.py create mode 100644 tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py310.py create mode 100644 tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py39.py diff --git a/docs/tutorial/fastapi/update-extra-data.md b/docs/tutorial/fastapi/update-extra-data.md new file mode 100644 index 0000000000..71d9b9cefa --- /dev/null +++ b/docs/tutorial/fastapi/update-extra-data.md @@ -0,0 +1,217 @@ +# Update with Extra Data (Hashed Passwords) with FastAPI + +In the previous chapter I explained to you how to update data in the database from input data coming from a **FastAPI** *path operation*. + +Now I'll explain to you how to add **extra data**, additional to the input data, when updating or creating a model object. + +This is particularly useful when you need to **generate some data** in your code that is **not coming from the client**, but you need to store it in the database. For example, to store a **hashed password**. + +## Password Hashing + +Let's imagine that each hero in our system also has a **password**. + +We should never store the password in plain text in the database, we should only stored a **hashed version** of it. + +"**Hashing**" means converting some content (a password in this case) into a sequence of bytes (just a string) that looks like gibberish. + +Whenever you pass exactly the same content (exactly the same password) you get exactly the same gibberish. + +But you **cannot convert** from the gibberish **back to the password**. + +### Why use Password Hashing + +If your database is stolen, the thief won't have your users' **plaintext passwords**, only the hashes. + +So, the thief won't be able to try to use that password in another system (as many users use the same password everywhere, this would be dangerous). + +/// tip + +You could use passlib to hash passwords. + +In this example we will use a fake hashing function to focus on the data changes. 🤡 + +/// + +## Update Models with Extra Data + +The `Hero` table model will now store a new field `hashed_password`. + +And the data models for `HeroCreate` and `HeroUpdate` will also have a new field `password` that will contain the plain text password sent by clients. + +```Python hl_lines="11 15 26" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/update/tutorial002.py[ln:7-30]!} + +# Code below omitted 👇 +``` + +/// details | 👀 Full file preview + +```Python +{!./docs_src/tutorial/fastapi/update/tutorial002.py!} +``` + +/// + +When a client is creating a new hero, they will send the `password` in the request body. + +And when they are updating a hero, they could also send the `password` in the request body to update it. + +## Hash the Password + +The app will receive the data from the client using the `HeroCreate` model. + +This contains the `password` field with the plain text password, and we cannot use that one. So we need to generate a hash from it. + +```Python hl_lines="11" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/update/tutorial002.py[ln:44-46]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/update/tutorial002.py[ln:57-59]!} + +# Code below omitted 👇 +``` + +/// details | 👀 Full file preview + +```Python +{!./docs_src/tutorial/fastapi/update/tutorial002.py!} +``` + +/// + +## Create an Object with Extra Data + +Now we need to create the database hero. + +In previous examples, we have used something like: + +```Python +db_hero = Hero.model_validate(hero) +``` + +This creates a `Hero` (which is a *table model*) object from the `HeroCreate` (which is a *data model*) object that we received in the request. + +And this is all good... but as `Hero` doesn't have a field `password`, it won't be extracted from the object `HeroCreate` that has it. + +`Hero` actually has a `hashed_password`, but we are not providing it. We need a way to provide it... + +### Dictionary Update + +Let's pause for a second to check this, when working with dictionaries, there's a way to `update` a dictionary with extra data from another dictionary, something like this: + +```Python hl_lines="14" +db_user_dict = { + "name": "Deadpond", + "secret_name": "Dive Wilson", + "age": None, +} + +hashed_password = "fakehashedpassword" + +extra_data = { + "hashed_password": hashed_password, + "age": 32, +} + +db_user_dict.update(extra_data) + +print(db_user_dict) + +# { +# "name": "Deadpond", +# "secret_name": "Dive Wilson", +# "age": 32, +# "hashed_password": "fakehashedpassword", +# } +``` + +This `update` method allows us to add and override things in the original dictionary with the data from another dictionary. + +So now, `db_user_dict` has the updated `age` field with `32` instead of `None` and more importantly, **it has the new `hashed_password` field**. + +### Create a Model Object with Extra Data + +Similar to how dictionaries have an `update` method, **SQLModel** models have a parameter `update` in `Hero.model_validate()` that takes a dictionary with extra data, or data that should take precedence: + +```Python hl_lines="8" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/update/tutorial002.py[ln:57-66]!} + +# Code below omitted 👇 +``` + +/// details | 👀 Full file preview + +```Python +{!./docs_src/tutorial/fastapi/update/tutorial002.py!} +``` + +/// + +Now, `db_hero` (which is a *table model* `Hero`) will extract its values from `hero` (which is a *data model* `HeroCreate`), and then it will **`update`** its values with the extra data from the dictionary `extra_data`. + +It will only take the fields defined in `Hero`, so **it will not take the `password`** from `HeroCreate`. And it will also **take its values** from the **dictionary passed to the `update`** parameter, in this case, the `hashed_password`. + +If there's a field in both `hero` and the `extra_data`, **the value from the `extra_data` passed to `update` will take precedence**. + +## Update with Extra Data + +Now let's say we want to **update a hero** that already exists in the database. + +The same way as before, to avoid removing existing data, we will use `exclude_unset=True` when calling `hero.model_dump()`, to get a dictionary with only the data sent by the client. + +```Python hl_lines="9" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/update/tutorial002.py[ln:85-91]!} + +# Code below omitted 👇 +``` + +/// details | 👀 Full file preview + +```Python +{!./docs_src/tutorial/fastapi/update/tutorial002.py!} +``` + +/// + +Now, this `hero_data` dictionary could contain a `password`. We need to check it, and if it's there, we need to generate the `hashed_password`. + +Then we can put that `hashed_password` in a dictionary. + +And then we can update the `db_hero` object using the method `db_hero.sqlmodel_update()`. + +It takes a model object or dictionary with the data to update the object and also an **additional `update` argument** with extra data. + +```Python hl_lines="15" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/update/tutorial002.py[ln:85-101]!} + +# Code below omitted 👇 +``` + +/// details | 👀 Full file preview + +```Python +{!./docs_src/tutorial/fastapi/update/tutorial002.py!} +``` + +/// + +/// tip + +The method `db_hero.sqlmodel_update()` was added in SQLModel 0.0.16. 😎 + +/// + +## Recap + +You can use the `update` parameter in `Hero.model_validate()` to provide extra data when creating a new object and `Hero.sqlmodel_update()` to provide extra data when updating an existing object. 🤓 diff --git a/docs/tutorial/fastapi/update.md b/docs/tutorial/fastapi/update.md index cfcf8a98e7..be4d90df16 100644 --- a/docs/tutorial/fastapi/update.md +++ b/docs/tutorial/fastapi/update.md @@ -154,12 +154,13 @@ Then we use that to get the data that was actually sent by the client: /// tip Before SQLModel 0.0.14, the method was called `hero.dict(exclude_unset=True)`, but it was renamed to `hero.model_dump(exclude_unset=True)` to be consistent with Pydantic v2. +/// ## Update the Hero in the Database -Now that we have a **dictionary with the data sent by the client**, we can iterate for each one of the keys and the values, and then we set them in the database hero model `db_hero` using `setattr()`. +Now that we have a **dictionary with the data sent by the client**, we can use the method `db_hero.sqlmodel_update()` to update the object `db_hero`. -```Python hl_lines="10-11" +```Python hl_lines="10" # Code above omitted 👆 {!./docs_src/tutorial/fastapi/update/tutorial001.py[ln:76-91]!} @@ -175,19 +176,17 @@ Now that we have a **dictionary with the data sent by the client**, we can itera /// -If you are not familiar with that `setattr()`, it takes an object, like the `db_hero`, then an attribute name (`key`), that in our case could be `"name"`, and a value (`value`). And then it **sets the attribute with that name to the value**. +/// tip -So, if `key` was `"name"` and `value` was `"Deadpuddle"`, then this code: +The method `db_hero.sqlmodel_update()` was added in SQLModel 0.0.16. 🤓 -```Python -setattr(db_hero, key, value) -``` +Before that, you would need to manually get the values and set them using `setattr()`. -...would be more or less equivalent to: +/// -```Python -db_hero.name = "Deadpuddle" -``` +The method `db_hero.sqlmodel_update()` takes an argument with another model object or a dictionary. + +For each of the fields in the **original** model object (`db_hero` in this example), it checks if the field is available in the **argument** (`hero_data` in this example) and then updates it with the provided value. ## Remove Fields diff --git a/docs_src/tutorial/fastapi/update/tutorial001.py b/docs_src/tutorial/fastapi/update/tutorial001.py index 5639638d5c..feab25cc13 100644 --- a/docs_src/tutorial/fastapi/update/tutorial001.py +++ b/docs_src/tutorial/fastapi/update/tutorial001.py @@ -80,8 +80,7 @@ def update_hero(hero_id: int, hero: HeroUpdate): if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") hero_data = hero.model_dump(exclude_unset=True) - for key, value in hero_data.items(): - setattr(db_hero, key, value) + db_hero.sqlmodel_update(hero_data) session.add(db_hero) session.commit() session.refresh(db_hero) diff --git a/docs_src/tutorial/fastapi/update/tutorial001_py310.py b/docs_src/tutorial/fastapi/update/tutorial001_py310.py index 4faf266f84..02bec2e0db 100644 --- a/docs_src/tutorial/fastapi/update/tutorial001_py310.py +++ b/docs_src/tutorial/fastapi/update/tutorial001_py310.py @@ -78,8 +78,7 @@ def update_hero(hero_id: int, hero: HeroUpdate): if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") hero_data = hero.model_dump(exclude_unset=True) - for key, value in hero_data.items(): - setattr(db_hero, key, value) + db_hero.sqlmodel_update(hero_data) session.add(db_hero) session.commit() session.refresh(db_hero) diff --git a/docs_src/tutorial/fastapi/update/tutorial001_py39.py b/docs_src/tutorial/fastapi/update/tutorial001_py39.py index b0daa87880..241d205c05 100644 --- a/docs_src/tutorial/fastapi/update/tutorial001_py39.py +++ b/docs_src/tutorial/fastapi/update/tutorial001_py39.py @@ -80,8 +80,7 @@ def update_hero(hero_id: int, hero: HeroUpdate): if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") hero_data = hero.model_dump(exclude_unset=True) - for key, value in hero_data.items(): - setattr(db_hero, key, value) + db_hero.sqlmodel_update(hero_data) session.add(db_hero) session.commit() session.refresh(db_hero) diff --git a/docs_src/tutorial/fastapi/update/tutorial002.py b/docs_src/tutorial/fastapi/update/tutorial002.py new file mode 100644 index 0000000000..1333654bcd --- /dev/null +++ b/docs_src/tutorial/fastapi/update/tutorial002.py @@ -0,0 +1,101 @@ +from typing import List, Optional + +from fastapi import FastAPI, HTTPException, Query +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class HeroBase(SQLModel): + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + +class Hero(HeroBase, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + hashed_password: str = Field() + + +class HeroCreate(HeroBase): + password: str + + +class HeroRead(HeroBase): + id: int + + +class HeroUpdate(SQLModel): + name: Optional[str] = None + secret_name: Optional[str] = None + age: Optional[int] = None + password: Optional[str] = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def hash_password(password: str) -> str: + # Use something like passlib here + return f"not really hashed {password} hehehe" + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroRead) +def create_hero(hero: HeroCreate): + hashed_password = hash_password(hero.password) + with Session(engine) as session: + extra_data = {"hashed_password": hashed_password} + db_hero = Hero.model_validate(hero, update=extra_data) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=List[HeroRead]) +def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): + with Session(engine) as session: + heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() + return heroes + + +@app.get("/heroes/{hero_id}", response_model=HeroRead) +def read_hero(hero_id: int): + with Session(engine) as session: + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + return hero + + +@app.patch("/heroes/{hero_id}", response_model=HeroRead) +def update_hero(hero_id: int, hero: HeroUpdate): + with Session(engine) as session: + db_hero = session.get(Hero, hero_id) + if not db_hero: + raise HTTPException(status_code=404, detail="Hero not found") + hero_data = hero.model_dump(exclude_unset=True) + extra_data = {} + if "password" in hero_data: + password = hero_data["password"] + hashed_password = hash_password(password) + extra_data["hashed_password"] = hashed_password + db_hero.sqlmodel_update(hero_data, update=extra_data) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero diff --git a/docs_src/tutorial/fastapi/update/tutorial002_py310.py b/docs_src/tutorial/fastapi/update/tutorial002_py310.py new file mode 100644 index 0000000000..84efb3d2a9 --- /dev/null +++ b/docs_src/tutorial/fastapi/update/tutorial002_py310.py @@ -0,0 +1,99 @@ +from fastapi import FastAPI, HTTPException, Query +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class HeroBase(SQLModel): + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +class Hero(HeroBase, table=True): + id: int | None = Field(default=None, primary_key=True) + hashed_password: str = Field() + + +class HeroCreate(HeroBase): + password: str + + +class HeroRead(HeroBase): + id: int + + +class HeroUpdate(SQLModel): + name: str | None = None + secret_name: str | None = None + age: int | None = None + password: str | None = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def hash_password(password: str) -> str: + # Use something like passlib here + return f"not really hashed {password} hehehe" + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroRead) +def create_hero(hero: HeroCreate): + hashed_password = hash_password(hero.password) + with Session(engine) as session: + extra_data = {"hashed_password": hashed_password} + db_hero = Hero.model_validate(hero, update=extra_data) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroRead]) +def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): + with Session(engine) as session: + heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() + return heroes + + +@app.get("/heroes/{hero_id}", response_model=HeroRead) +def read_hero(hero_id: int): + with Session(engine) as session: + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + return hero + + +@app.patch("/heroes/{hero_id}", response_model=HeroRead) +def update_hero(hero_id: int, hero: HeroUpdate): + with Session(engine) as session: + db_hero = session.get(Hero, hero_id) + if not db_hero: + raise HTTPException(status_code=404, detail="Hero not found") + hero_data = hero.model_dump(exclude_unset=True) + update_data = {} + if "password" in hero_data: + password = hero_data["password"] + hashed_password = hash_password(password) + update_data["hashed_password"] = hashed_password + db_hero.sqlmodel_update(hero_data, update=update_data) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero diff --git a/docs_src/tutorial/fastapi/update/tutorial002_py39.py b/docs_src/tutorial/fastapi/update/tutorial002_py39.py new file mode 100644 index 0000000000..72751dac3d --- /dev/null +++ b/docs_src/tutorial/fastapi/update/tutorial002_py39.py @@ -0,0 +1,101 @@ +from typing import Optional + +from fastapi import FastAPI, HTTPException, Query +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class HeroBase(SQLModel): + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + +class Hero(HeroBase, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + hashed_password: str = Field() + + +class HeroCreate(HeroBase): + password: str + + +class HeroRead(HeroBase): + id: int + + +class HeroUpdate(SQLModel): + name: Optional[str] = None + secret_name: Optional[str] = None + age: Optional[int] = None + password: Optional[str] = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +connect_args = {"check_same_thread": False} +engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def hash_password(password: str) -> str: + # Use something like passlib here + return f"not really hashed {password} hehehe" + + +app = FastAPI() + + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + + +@app.post("/heroes/", response_model=HeroRead) +def create_hero(hero: HeroCreate): + hashed_password = hash_password(hero.password) + with Session(engine) as session: + extra_data = {"hashed_password": hashed_password} + db_hero = Hero.model_validate(hero, update=extra_data) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + + +@app.get("/heroes/", response_model=list[HeroRead]) +def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): + with Session(engine) as session: + heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() + return heroes + + +@app.get("/heroes/{hero_id}", response_model=HeroRead) +def read_hero(hero_id: int): + with Session(engine) as session: + hero = session.get(Hero, hero_id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + return hero + + +@app.patch("/heroes/{hero_id}", response_model=HeroRead) +def update_hero(hero_id: int, hero: HeroUpdate): + with Session(engine) as session: + db_hero = session.get(Hero, hero_id) + if not db_hero: + raise HTTPException(status_code=404, detail="Hero not found") + hero_data = hero.model_dump(exclude_unset=True) + update_data = {} + if "password" in hero_data: + password = hero_data["password"] + hashed_password = hash_password(password) + update_data["hashed_password"] = hashed_password + db_hero.sqlmodel_update(hero_data, update=update_data) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero diff --git a/mkdocs.yml b/mkdocs.yml index ce98f1524e..fa85062a8b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -90,6 +90,7 @@ nav: - tutorial/fastapi/read-one.md - tutorial/fastapi/limit-and-offset.md - tutorial/fastapi/update.md + - tutorial/fastapi/update-extra-data.md - tutorial/fastapi/delete.md - tutorial/fastapi/session-with-dependency.md - tutorial/fastapi/teams.md diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index 76771ce7ff..072d2b0f58 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -6,6 +6,7 @@ TYPE_CHECKING, AbstractSet, Any, + Callable, Dict, ForwardRef, Generator, @@ -18,6 +19,7 @@ ) from pydantic import VERSION as PYDANTIC_VERSION +from pydantic import BaseModel from pydantic.fields import FieldInfo from typing_extensions import get_args, get_origin @@ -46,9 +48,11 @@ class ObjectWithUpdateWrapper: update: Dict[str, Any] def __getattribute__(self, __name: str) -> Any: - if __name in self.update: - return self.update[__name] - return getattr(self.obj, __name) + update = super().__getattribute__("update") + obj = super().__getattribute__("obj") + if __name in update: + return update[__name] + return getattr(obj, __name) def _is_union_type(t: Any) -> bool: @@ -94,9 +98,14 @@ def set_config_value( ) -> None: model.model_config[parameter] = value # type: ignore[literal-required] - def get_model_fields(model: InstanceOrType["SQLModel"]) -> Dict[str, "FieldInfo"]: + def get_model_fields(model: InstanceOrType[BaseModel]) -> Dict[str, "FieldInfo"]: return model.model_fields + def get_fields_set( + object: InstanceOrType["SQLModel"], + ) -> Union[Set[str], Callable[[BaseModel], Set[str]]]: + return object.model_fields_set + def init_pydantic_private_attrs(new_object: InstanceOrType["SQLModel"]) -> None: object.__setattr__(new_object, "__pydantic_fields_set__", set()) object.__setattr__(new_object, "__pydantic_extra__", None) @@ -384,9 +393,14 @@ def set_config_value( ) -> None: setattr(model.__config__, parameter, value) # type: ignore - def get_model_fields(model: InstanceOrType["SQLModel"]) -> Dict[str, "FieldInfo"]: + def get_model_fields(model: InstanceOrType[BaseModel]) -> Dict[str, "FieldInfo"]: return model.__fields__ # type: ignore + def get_fields_set( + object: InstanceOrType["SQLModel"], + ) -> Union[Set[str], Callable[[BaseModel], Set[str]]]: + return object.__fields_set__ + def init_pydantic_private_attrs(new_object: InstanceOrType["SQLModel"]) -> None: object.__setattr__(new_object, "__fields_set__", set()) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index fec3bc7906..9e8330d69d 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -758,7 +758,6 @@ def model_validate( update=update, ) - # TODO: remove when deprecating Pydantic v1, only for compatibility def model_dump( self, *, @@ -869,3 +868,32 @@ def _calculate_keys( exclude_unset=exclude_unset, update=update, ) + + def sqlmodel_update( + self: _TSQLModel, + obj: Union[Dict[str, Any], BaseModel], + *, + update: Union[Dict[str, Any], None] = None, + ) -> _TSQLModel: + use_update = (update or {}).copy() + if isinstance(obj, dict): + for key, value in {**obj, **use_update}.items(): + if key in get_model_fields(self): + setattr(self, key, value) + elif isinstance(obj, BaseModel): + for key in get_model_fields(obj): + if key in use_update: + value = use_update.pop(key) + else: + value = getattr(obj, key) + setattr(self, key, value) + for remaining_key in use_update: + if remaining_key in get_model_fields(self): + value = use_update.pop(remaining_key) + setattr(self, remaining_key, value) + else: + raise ValueError( + "Can't use sqlmodel_update() with something that " + f"is not a dict or SQLModel or Pydantic model: {obj}" + ) + return self diff --git a/tests/test_fields_set.py b/tests/test_fields_set.py index 56f4ad0144..e0bd8cba76 100644 --- a/tests/test_fields_set.py +++ b/tests/test_fields_set.py @@ -1,6 +1,7 @@ from datetime import datetime, timedelta from sqlmodel import Field, SQLModel +from sqlmodel._compat import get_fields_set def test_fields_set(): @@ -10,12 +11,12 @@ class User(SQLModel): last_updated: datetime = Field(default_factory=datetime.now) user = User(username="bob") - assert user.__fields_set__ == {"username"} + assert get_fields_set(user) == {"username"} user = User(username="bob", email="bob@test.com") - assert user.__fields_set__ == {"username", "email"} + assert get_fields_set(user) == {"username", "email"} user = User( username="bob", email="bob@test.com", last_updated=datetime.now() - timedelta(days=1), ) - assert user.__fields_set__ == {"username", "email", "last_updated"} + assert get_fields_set(user) == {"username", "email", "last_updated"} diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial002.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial002.py new file mode 100644 index 0000000000..21ca74e53f --- /dev/null +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial002.py @@ -0,0 +1,427 @@ +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from sqlmodel import Session, create_engine +from sqlmodel.pool import StaticPool + + +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.fastapi.update import tutorial002 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine( + mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool + ) + + with TestClient(mod.app) as client: + hero1_data = { + "name": "Deadpond", + "secret_name": "Dive Wilson", + "password": "chimichanga", + } + hero2_data = { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "id": 9000, + "password": "auntmay", + } + hero3_data = { + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "age": 48, + "password": "bestpreventer", + } + response = client.post("/heroes/", json=hero1_data) + assert response.status_code == 200, response.text + hero1 = response.json() + assert "password" not in hero1 + assert "hashed_password" not in hero1 + hero1_id = hero1["id"] + response = client.post("/heroes/", json=hero2_data) + assert response.status_code == 200, response.text + hero2 = response.json() + hero2_id = hero2["id"] + response = client.post("/heroes/", json=hero3_data) + assert response.status_code == 200, response.text + hero3 = response.json() + hero3_id = hero3["id"] + response = client.get(f"/heroes/{hero2_id}") + assert response.status_code == 200, response.text + fetched_hero2 = response.json() + assert "password" not in fetched_hero2 + assert "hashed_password" not in fetched_hero2 + response = client.get("/heroes/9000") + assert response.status_code == 404, response.text + response = client.get("/heroes/") + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 3 + for response_hero in data: + assert "password" not in response_hero + assert "hashed_password" not in response_hero + + # Test hashed passwords + with Session(mod.engine) as session: + hero1_db = session.get(mod.Hero, hero1_id) + assert hero1_db + assert not hasattr(hero1_db, "password") + assert hero1_db.hashed_password == "not really hashed chimichanga hehehe" + hero2_db = session.get(mod.Hero, hero2_id) + assert hero2_db + assert not hasattr(hero2_db, "password") + assert hero2_db.hashed_password == "not really hashed auntmay hehehe" + hero3_db = session.get(mod.Hero, hero3_id) + assert hero3_db + assert not hasattr(hero3_db, "password") + assert hero3_db.hashed_password == "not really hashed bestpreventer hehehe" + + response = client.patch( + f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} + ) + data = response.json() + assert response.status_code == 200, response.text + assert data["name"] == hero2_data["name"], "The name should not be set to none" + assert ( + data["secret_name"] == "Spider-Youngster" + ), "The secret name should be updated" + assert "password" not in data + assert "hashed_password" not in data + with Session(mod.engine) as session: + hero2b_db = session.get(mod.Hero, hero2_id) + assert hero2b_db + assert not hasattr(hero2b_db, "password") + assert hero2b_db.hashed_password == "not really hashed auntmay hehehe" + + response = client.patch(f"/heroes/{hero3_id}", json={"age": None}) + data = response.json() + assert response.status_code == 200, response.text + assert data["name"] == hero3_data["name"] + assert ( + data["age"] is None + ), "A field should be updatable to None, even if that's the default" + assert "password" not in data + assert "hashed_password" not in data + with Session(mod.engine) as session: + hero3b_db = session.get(mod.Hero, hero3_id) + assert hero3b_db + assert not hasattr(hero3b_db, "password") + assert hero3b_db.hashed_password == "not really hashed bestpreventer hehehe" + + # Test update dict, hashed_password + response = client.patch( + f"/heroes/{hero3_id}", json={"password": "philantroplayboy"} + ) + data = response.json() + assert response.status_code == 200, response.text + assert data["name"] == hero3_data["name"] + assert data["age"] is None + assert "password" not in data + assert "hashed_password" not in data + with Session(mod.engine) as session: + hero3b_db = session.get(mod.Hero, hero3_id) + assert hero3b_db + assert not hasattr(hero3b_db, "password") + assert ( + hero3b_db.hashed_password == "not really hashed philantroplayboy hehehe" + ) + + response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) + assert response.status_code == 404, response.text + + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Offset", + "type": "integer", + "default": 0, + }, + "name": "offset", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "maximum": 100, + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Heroes Heroes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/HeroRead" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/heroes/{hero_id}": { + "get": { + "summary": "Read Hero", + "operationId": "read_hero_heroes__hero_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "patch": { + "summary": "Update Hero", + "operationId": "update_hero_heroes__hero_id__patch", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroUpdate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "HeroCreate": { + "title": "HeroCreate", + "required": ["name", "secret_name", "password"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": IsDict( + { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Age", + } + ) + | IsDict( + # TODO: Remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), + "password": {"type": "string", "title": "Password"}, + }, + }, + "HeroRead": { + "title": "HeroRead", + "required": ["name", "secret_name", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": IsDict( + { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Age", + } + ) + | IsDict( + # TODO: Remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), + "id": {"title": "Id", "type": "integer"}, + }, + }, + "HeroUpdate": { + "title": "HeroUpdate", + "type": "object", + "properties": { + "name": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Name", + } + ) + | IsDict( + # TODO: Remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ), + "secret_name": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Secret Name", + } + ) + | IsDict( + # TODO: Remove when deprecating Pydantic v1 + {"title": "Secret Name", "type": "string"} + ), + "age": IsDict( + { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Age", + } + ) + | IsDict( + # TODO: Remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), + "password": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Password", + } + ) + | IsDict( + # TODO: Remove when deprecating Pydantic v1 + {"title": "Password", "type": "string"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py310.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py310.py new file mode 100644 index 0000000000..6feb1ec54e --- /dev/null +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py310.py @@ -0,0 +1,430 @@ +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from sqlmodel import Session, create_engine +from sqlmodel.pool import StaticPool + +from ....conftest import needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.fastapi.update import tutorial002_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine( + mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool + ) + + with TestClient(mod.app) as client: + hero1_data = { + "name": "Deadpond", + "secret_name": "Dive Wilson", + "password": "chimichanga", + } + hero2_data = { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "id": 9000, + "password": "auntmay", + } + hero3_data = { + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "age": 48, + "password": "bestpreventer", + } + response = client.post("/heroes/", json=hero1_data) + assert response.status_code == 200, response.text + hero1 = response.json() + assert "password" not in hero1 + assert "hashed_password" not in hero1 + hero1_id = hero1["id"] + response = client.post("/heroes/", json=hero2_data) + assert response.status_code == 200, response.text + hero2 = response.json() + hero2_id = hero2["id"] + response = client.post("/heroes/", json=hero3_data) + assert response.status_code == 200, response.text + hero3 = response.json() + hero3_id = hero3["id"] + response = client.get(f"/heroes/{hero2_id}") + assert response.status_code == 200, response.text + fetched_hero2 = response.json() + assert "password" not in fetched_hero2 + assert "hashed_password" not in fetched_hero2 + response = client.get("/heroes/9000") + assert response.status_code == 404, response.text + response = client.get("/heroes/") + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 3 + for response_hero in data: + assert "password" not in response_hero + assert "hashed_password" not in response_hero + + # Test hashed passwords + with Session(mod.engine) as session: + hero1_db = session.get(mod.Hero, hero1_id) + assert hero1_db + assert not hasattr(hero1_db, "password") + assert hero1_db.hashed_password == "not really hashed chimichanga hehehe" + hero2_db = session.get(mod.Hero, hero2_id) + assert hero2_db + assert not hasattr(hero2_db, "password") + assert hero2_db.hashed_password == "not really hashed auntmay hehehe" + hero3_db = session.get(mod.Hero, hero3_id) + assert hero3_db + assert not hasattr(hero3_db, "password") + assert hero3_db.hashed_password == "not really hashed bestpreventer hehehe" + + response = client.patch( + f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} + ) + data = response.json() + assert response.status_code == 200, response.text + assert data["name"] == hero2_data["name"], "The name should not be set to none" + assert ( + data["secret_name"] == "Spider-Youngster" + ), "The secret name should be updated" + assert "password" not in data + assert "hashed_password" not in data + with Session(mod.engine) as session: + hero2b_db = session.get(mod.Hero, hero2_id) + assert hero2b_db + assert not hasattr(hero2b_db, "password") + assert hero2b_db.hashed_password == "not really hashed auntmay hehehe" + + response = client.patch(f"/heroes/{hero3_id}", json={"age": None}) + data = response.json() + assert response.status_code == 200, response.text + assert data["name"] == hero3_data["name"] + assert ( + data["age"] is None + ), "A field should be updatable to None, even if that's the default" + assert "password" not in data + assert "hashed_password" not in data + with Session(mod.engine) as session: + hero3b_db = session.get(mod.Hero, hero3_id) + assert hero3b_db + assert not hasattr(hero3b_db, "password") + assert hero3b_db.hashed_password == "not really hashed bestpreventer hehehe" + + # Test update dict, hashed_password + response = client.patch( + f"/heroes/{hero3_id}", json={"password": "philantroplayboy"} + ) + data = response.json() + assert response.status_code == 200, response.text + assert data["name"] == hero3_data["name"] + assert data["age"] is None + assert "password" not in data + assert "hashed_password" not in data + with Session(mod.engine) as session: + hero3b_db = session.get(mod.Hero, hero3_id) + assert hero3b_db + assert not hasattr(hero3b_db, "password") + assert ( + hero3b_db.hashed_password == "not really hashed philantroplayboy hehehe" + ) + + response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) + assert response.status_code == 404, response.text + + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Offset", + "type": "integer", + "default": 0, + }, + "name": "offset", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "maximum": 100, + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Heroes Heroes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/HeroRead" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/heroes/{hero_id}": { + "get": { + "summary": "Read Hero", + "operationId": "read_hero_heroes__hero_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "patch": { + "summary": "Update Hero", + "operationId": "update_hero_heroes__hero_id__patch", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroUpdate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "HeroCreate": { + "title": "HeroCreate", + "required": ["name", "secret_name", "password"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": IsDict( + { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Age", + } + ) + | IsDict( + # TODO: Remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), + "password": {"type": "string", "title": "Password"}, + }, + }, + "HeroRead": { + "title": "HeroRead", + "required": ["name", "secret_name", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": IsDict( + { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Age", + } + ) + | IsDict( + # TODO: Remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), + "id": {"title": "Id", "type": "integer"}, + }, + }, + "HeroUpdate": { + "title": "HeroUpdate", + "type": "object", + "properties": { + "name": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Name", + } + ) + | IsDict( + # TODO: Remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ), + "secret_name": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Secret Name", + } + ) + | IsDict( + # TODO: Remove when deprecating Pydantic v1 + {"title": "Secret Name", "type": "string"} + ), + "age": IsDict( + { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Age", + } + ) + | IsDict( + # TODO: Remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), + "password": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Password", + } + ) + | IsDict( + # TODO: Remove when deprecating Pydantic v1 + {"title": "Password", "type": "string"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py39.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py39.py new file mode 100644 index 0000000000..13d70dd3e8 --- /dev/null +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py39.py @@ -0,0 +1,430 @@ +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from sqlmodel import Session, create_engine +from sqlmodel.pool import StaticPool + +from ....conftest import needs_py39 + + +@needs_py39 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.fastapi.update import tutorial002_py39 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine( + mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool + ) + + with TestClient(mod.app) as client: + hero1_data = { + "name": "Deadpond", + "secret_name": "Dive Wilson", + "password": "chimichanga", + } + hero2_data = { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "id": 9000, + "password": "auntmay", + } + hero3_data = { + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "age": 48, + "password": "bestpreventer", + } + response = client.post("/heroes/", json=hero1_data) + assert response.status_code == 200, response.text + hero1 = response.json() + assert "password" not in hero1 + assert "hashed_password" not in hero1 + hero1_id = hero1["id"] + response = client.post("/heroes/", json=hero2_data) + assert response.status_code == 200, response.text + hero2 = response.json() + hero2_id = hero2["id"] + response = client.post("/heroes/", json=hero3_data) + assert response.status_code == 200, response.text + hero3 = response.json() + hero3_id = hero3["id"] + response = client.get(f"/heroes/{hero2_id}") + assert response.status_code == 200, response.text + fetched_hero2 = response.json() + assert "password" not in fetched_hero2 + assert "hashed_password" not in fetched_hero2 + response = client.get("/heroes/9000") + assert response.status_code == 404, response.text + response = client.get("/heroes/") + assert response.status_code == 200, response.text + data = response.json() + assert len(data) == 3 + for response_hero in data: + assert "password" not in response_hero + assert "hashed_password" not in response_hero + + # Test hashed passwords + with Session(mod.engine) as session: + hero1_db = session.get(mod.Hero, hero1_id) + assert hero1_db + assert not hasattr(hero1_db, "password") + assert hero1_db.hashed_password == "not really hashed chimichanga hehehe" + hero2_db = session.get(mod.Hero, hero2_id) + assert hero2_db + assert not hasattr(hero2_db, "password") + assert hero2_db.hashed_password == "not really hashed auntmay hehehe" + hero3_db = session.get(mod.Hero, hero3_id) + assert hero3_db + assert not hasattr(hero3_db, "password") + assert hero3_db.hashed_password == "not really hashed bestpreventer hehehe" + + response = client.patch( + f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} + ) + data = response.json() + assert response.status_code == 200, response.text + assert data["name"] == hero2_data["name"], "The name should not be set to none" + assert ( + data["secret_name"] == "Spider-Youngster" + ), "The secret name should be updated" + assert "password" not in data + assert "hashed_password" not in data + with Session(mod.engine) as session: + hero2b_db = session.get(mod.Hero, hero2_id) + assert hero2b_db + assert not hasattr(hero2b_db, "password") + assert hero2b_db.hashed_password == "not really hashed auntmay hehehe" + + response = client.patch(f"/heroes/{hero3_id}", json={"age": None}) + data = response.json() + assert response.status_code == 200, response.text + assert data["name"] == hero3_data["name"] + assert ( + data["age"] is None + ), "A field should be updatable to None, even if that's the default" + assert "password" not in data + assert "hashed_password" not in data + with Session(mod.engine) as session: + hero3b_db = session.get(mod.Hero, hero3_id) + assert hero3b_db + assert not hasattr(hero3b_db, "password") + assert hero3b_db.hashed_password == "not really hashed bestpreventer hehehe" + + # Test update dict, hashed_password + response = client.patch( + f"/heroes/{hero3_id}", json={"password": "philantroplayboy"} + ) + data = response.json() + assert response.status_code == 200, response.text + assert data["name"] == hero3_data["name"] + assert data["age"] is None + assert "password" not in data + assert "hashed_password" not in data + with Session(mod.engine) as session: + hero3b_db = session.get(mod.Hero, hero3_id) + assert hero3b_db + assert not hasattr(hero3b_db, "password") + assert ( + hero3b_db.hashed_password == "not really hashed philantroplayboy hehehe" + ) + + response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) + assert response.status_code == 404, response.text + + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/heroes/": { + "get": { + "summary": "Read Heroes", + "operationId": "read_heroes_heroes__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Offset", + "type": "integer", + "default": 0, + }, + "name": "offset", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "maximum": 100, + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Heroes Heroes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/HeroRead" + }, + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "post": { + "summary": "Create Hero", + "operationId": "create_hero_heroes__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroCreate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/heroes/{hero_id}": { + "get": { + "summary": "Read Hero", + "operationId": "read_hero_heroes__hero_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "patch": { + "summary": "Update Hero", + "operationId": "update_hero_heroes__hero_id__patch", + "parameters": [ + { + "required": True, + "schema": {"title": "Hero Id", "type": "integer"}, + "name": "hero_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroUpdate" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HeroRead" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "HeroCreate": { + "title": "HeroCreate", + "required": ["name", "secret_name", "password"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": IsDict( + { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Age", + } + ) + | IsDict( + # TODO: Remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), + "password": {"type": "string", "title": "Password"}, + }, + }, + "HeroRead": { + "title": "HeroRead", + "required": ["name", "secret_name", "id"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "secret_name": {"title": "Secret Name", "type": "string"}, + "age": IsDict( + { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Age", + } + ) + | IsDict( + # TODO: Remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), + "id": {"title": "Id", "type": "integer"}, + }, + }, + "HeroUpdate": { + "title": "HeroUpdate", + "type": "object", + "properties": { + "name": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Name", + } + ) + | IsDict( + # TODO: Remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ), + "secret_name": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Secret Name", + } + ) + | IsDict( + # TODO: Remove when deprecating Pydantic v1 + {"title": "Secret Name", "type": "string"} + ), + "age": IsDict( + { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Age", + } + ) + | IsDict( + # TODO: Remove when deprecating Pydantic v1 + {"title": "Age", "type": "integer"} + ), + "password": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Password", + } + ) + | IsDict( + # TODO: Remove when deprecating Pydantic v1 + {"title": "Password", "type": "string"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } From 2abb798a22733456e4561f062e28ba61be214a7b Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 17 Feb 2024 13:49:59 +0000 Subject: [PATCH 335/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index a0431a9bd7..bdd0cfc39c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Features + +* ✨ Add new method `sqlmodel_update()` to update models in place, including an `update` parameter for extra data. PR [#804](https://github.com/tiangolo/sqlmodel/pull/804) by [@tiangolo](https://github.com/tiangolo). + ## 0.0.15 ### Fixes From 3d483921feb151b4961634fee839daff4a02dabc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 17 Feb 2024 14:52:43 +0100 Subject: [PATCH 336/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index bdd0cfc39c..1da7afb815 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,7 +4,9 @@ ### Features -* ✨ Add new method `sqlmodel_update()` to update models in place, including an `update` parameter for extra data. PR [#804](https://github.com/tiangolo/sqlmodel/pull/804) by [@tiangolo](https://github.com/tiangolo). +* ✨ Add new method `.sqlmodel_update()` to update models in place, including an `update` parameter for extra data. And fix implementation for the (now documented) `update` parameter for `.model_validate()`. PR [#804](https://github.com/tiangolo/sqlmodel/pull/804) by [@tiangolo](https://github.com/tiangolo). + * Updated docs: [Update Data with FastAPI](https://sqlmodel.tiangolo.com/tutorial/fastapi/update/). + * New docs: [Update with Extra Data (Hashed Passwords) with FastAPI](https://sqlmodel.tiangolo.com/tutorial/fastapi/update-extra-data/). ## 0.0.15 From 6b562358fc1e857dd1ce4b8b23a9f68c0337430d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 17 Feb 2024 14:53:16 +0100 Subject: [PATCH 337/906] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.0.?= =?UTF-8?q?16?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 2 ++ sqlmodel/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 1da7afb815..d97266148e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest Changes +## 0.0.16 + ### Features * ✨ Add new method `.sqlmodel_update()` to update models in place, including an `update` parameter for extra data. And fix implementation for the (now documented) `update` parameter for `.model_validate()`. PR [#804](https://github.com/tiangolo/sqlmodel/pull/804) by [@tiangolo](https://github.com/tiangolo). diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index c9629a98b1..556bba1895 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.0.15" +__version__ = "0.0.16" # Re-export from SQLAlchemy from sqlalchemy.engine import create_engine as create_engine From 0a80166b81dcfa7d3356b8c9966e9a903c2ba3c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 13 Mar 2024 20:31:20 +0100 Subject: [PATCH 338/906] =?UTF-8?q?=F0=9F=94=A5=20Remove=20Jina=20QA=20Bot?= =?UTF-8?q?=20as=20it=20has=20been=20discontinued=20(#840)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/overrides/main.html | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/docs/overrides/main.html b/docs/overrides/main.html index 7440b084ad..94d9808cc7 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -1,31 +1 @@ {% extends "base.html" %} -{%- block scripts %} -{{ super() }} - - - - - -{%- endblock %} From 4c3f242ae215b28ef7b1dcb37411531999826d0d Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 13 Mar 2024 19:31:41 +0000 Subject: [PATCH 339/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index d97266148e..7609024f16 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Internal + +* 🔥 Remove Jina QA Bot as it has been discontinued. PR [#840](https://github.com/tiangolo/sqlmodel/pull/840) by [@tiangolo](https://github.com/tiangolo). + ## 0.0.16 ### Features From 9141c8a920ab291b0761c53b3440767ddb3a60ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 21 Mar 2024 17:49:38 -0500 Subject: [PATCH 340/906] =?UTF-8?q?=E2=9C=A8=20Add=20source=20examples=20f?= =?UTF-8?q?or=20Python=203.10=20and=203.9=20with=20updated=20syntax=20(#84?= =?UTF-8?q?2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Esteban Maya Cadavid --- docs/advanced/decimal.md | 90 ++- docs/tutorial/automatic-id-none-refresh.md | 247 +++++++- .../tutorial/connect/create-connected-rows.md | 155 ++++- .../connect/create-connected-tables.md | 104 ++++ docs/tutorial/connect/read-connected-data.md | 208 +++++++ .../connect/remove-data-connections.md | 44 ++ .../connect/update-data-connections.md | 44 ++ docs/tutorial/create-db-and-table.md | 264 ++++++++- docs/tutorial/delete.md | 220 +++++++ docs/tutorial/fastapi/delete.md | 48 ++ docs/tutorial/fastapi/limit-and-offset.md | 52 ++ docs/tutorial/fastapi/multiple-models.md | 424 ++++++++++++++ docs/tutorial/fastapi/read-one.md | 144 +++++ docs/tutorial/fastapi/relationships.md | 232 ++++++++ docs/tutorial/fastapi/response-model.md | 98 ++++ .../fastapi/session-with-dependency.md | 332 +++++++++++ docs/tutorial/fastapi/simple-hero-api.md | 142 +++++ docs/tutorial/fastapi/teams.md | 190 +++++- docs/tutorial/fastapi/update-extra-data.md | 248 ++++++++ docs/tutorial/fastapi/update.md | 240 ++++++++ docs/tutorial/indexes.md | 118 ++++ docs/tutorial/insert.md | 273 +++++++++ docs/tutorial/limit-and-offset.md | 168 ++++++ docs/tutorial/many-to-many/create-data.md | 166 +++++- .../many-to-many/create-models-with-link.md | 238 ++++++++ .../many-to-many/link-with-extra-fields.md | 296 ++++++++++ .../update-remove-relationships.md | 212 +++++++ docs/tutorial/one.md | 292 ++++++++++ .../relationship-attributes/back-populates.md | 540 ++++++++++++++++++ .../create-and-update-relationships.md | 244 ++++++++ .../define-relationships-attributes.md | 114 ++++ .../read-relationships.md | 208 +++++++ .../remove-relationships.md | 96 ++++ .../type-annotation-strings.md | 44 ++ docs/tutorial/select.md | 250 ++++++++ docs/tutorial/update.md | 250 ++++++++ docs/tutorial/where.md | 428 ++++++++++++++ .../fastapi/update/tutorial002_py310.py | 6 +- .../fastapi/update/tutorial002_py39.py | 6 +- 39 files changed, 7453 insertions(+), 22 deletions(-) diff --git a/docs/advanced/decimal.md b/docs/advanced/decimal.md index 2b58550d7f..26994eccf8 100644 --- a/docs/advanced/decimal.md +++ b/docs/advanced/decimal.md @@ -33,18 +33,44 @@ For the database, **SQLModel** will use @@ -324,7 +491,21 @@ But what if you want to **explicitly refresh** the data? You can do that too with `session.refresh(object)`: -```Python hl_lines="30-32 35-37" +//// tab | Python 3.10+ + +```Python hl_lines="30-32 35-37" +# Code above omitted 👆 + +{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py[ln:31-65]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + +```Python hl_lines="30-32 35-37" # Code above omitted 👆 {!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py[ln:33-67]!} @@ -332,12 +513,26 @@ You can do that too with `session.refresh(object)`: # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py!} ``` +//// + /// When Python executes this code: @@ -396,6 +591,20 @@ Now, as a final experiment, we can also print data after the **session** is clos There are no surprises here, it still works: +//// tab | Python 3.10+ + +```Python hl_lines="40-42" +# Code above omitted 👆 + +{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py[ln:31-70]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="40-42" # Code above omitted 👆 @@ -404,12 +613,26 @@ There are no surprises here, it still works: # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/automatic_id_none_refresh/tutorial001.py!} ``` +//// + /// And the output shows again the same data: @@ -445,12 +668,26 @@ And as we created the **engine** with `echo=True`, we can see the SQL statements /// -```{ .python .annotate } +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/automatic_id_none_refresh/tutorial002_py310.py!} +``` + +{!./docs_src/tutorial/automatic_id_none_refresh/annotations/en/tutorial002.md!} + +//// + +//// tab | Python 3.7+ + +```Python {!./docs_src/tutorial/automatic_id_none_refresh/tutorial002.py!} ``` {!./docs_src/tutorial/automatic_id_none_refresh/annotations/en/tutorial002.md!} +//// + And here's all the output generated by running this program, all together:
diff --git a/docs/tutorial/connect/create-connected-rows.md b/docs/tutorial/connect/create-connected-rows.md index e2818c95e8..d72c0b2247 100644 --- a/docs/tutorial/connect/create-connected-rows.md +++ b/docs/tutorial/connect/create-connected-rows.md @@ -47,10 +47,22 @@ We will continue with the code in the previous example and we will add more thin /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/connect/create_tables/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/connect/create_tables/tutorial001.py!} ``` +//// + /// Make sure you remove the `database.db` file before running the examples to get the same results. @@ -63,6 +75,20 @@ And now we will also create the teams there. 🎉 Let's start by creating two teams: +//// tab | Python 3.10+ + +```Python hl_lines="3-9" +# Code above omitted 👆 + +{!./docs_src/tutorial/connect/insert/tutorial001_py310.py[ln:29-35]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3-9" # Code above omitted 👆 @@ -71,12 +97,26 @@ Let's start by creating two teams: # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/connect/insert/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/connect/insert/tutorial001.py!} ``` +//// + /// This would hopefully look already familiar. @@ -93,6 +133,20 @@ And finally we **commit** the session to save the changes to the database. Let's not forget to add this function `create_heroes()` to the `main()` function so that we run it when calling the program from the command line: +//// tab | Python 3.10+ + +```Python hl_lines="5" +# Code above omitted 👆 + +{!./docs_src/tutorial/connect/insert/tutorial001_py310.py[ln:61-63]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="5" # Code above omitted 👆 @@ -101,12 +155,26 @@ Let's not forget to add this function `create_heroes()` to the `main()` function # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/connect/insert/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/connect/insert/tutorial001.py!} ``` +//// + /// ## Run it @@ -140,6 +208,20 @@ Now let's create one hero object to start. As the `Hero` class model now has a field (column, attribute) `team_id`, we can set it by using the ID field from the `Team` objects we just created before: +//// tab | Python 3.10+ + +```Python hl_lines="12" +# Code above omitted 👆 + +{!./docs_src/tutorial/connect/insert/tutorial001_py310.py[ln:29-39]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="12" # Code above omitted 👆 @@ -148,12 +230,26 @@ As the `Hero` class model now has a field (column, attribute) `team_id`, we can # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/connect/insert/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/connect/insert/tutorial001.py!} ``` +//// + /// We haven't committed this hero to the database yet, but there are already a couple of things to pay **attention** to. @@ -178,6 +274,20 @@ INFO Engine [generated in 0.00025s] (2,) Let's now create two more heroes: +//// tab | Python 3.10+ + +```Python hl_lines="14-20" +# Code above omitted 👆 + +{!./docs_src/tutorial/connect/insert/tutorial001_py310.py[ln:29-50]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="14-20" # Code above omitted 👆 @@ -186,12 +296,26 @@ Let's now create two more heroes: # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/connect/insert/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/connect/insert/tutorial001.py!} ``` +//// + /// When creating `hero_rusty_man`, we are accessing `team_preventers.id`, so that will also trigger a refresh of its data, generating an output of: @@ -223,7 +347,21 @@ INFO Engine COMMIT Now let's refresh and print those new heroes to see their new ID pointing to their teams: -```Python hl_lines="26-28 30-32" +//// tab | Python 3.10+ + +```Python hl_lines="26-28 30-32" +# Code above omitted 👆 + +{!./docs_src/tutorial/connect/insert/tutorial001_py310.py[ln:29-58]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + +```Python hl_lines="26-28 30-32" # Code above omitted 👆 {!./docs_src/tutorial/connect/insert/tutorial001.py[ln:31-60]!} @@ -231,14 +369,27 @@ Now let's refresh and print those new heroes to see their new ID pointing to the # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/connect/insert/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/connect/insert/tutorial001.py!} ``` -/// +//// +/// If we execute that in the command line, it will output: diff --git a/docs/tutorial/connect/create-connected-tables.md b/docs/tutorial/connect/create-connected-tables.md index a27cbfed7a..42e870f706 100644 --- a/docs/tutorial/connect/create-connected-tables.md +++ b/docs/tutorial/connect/create-connected-tables.md @@ -57,18 +57,44 @@ Let's start by creating the tables in code. Import the things we need from `sqlmodel` and create a new `Team` model: +//// tab | Python 3.10+ + +```Python hl_lines="4-7" +{!./docs_src/tutorial/connect/create_tables/tutorial001_py310.py[ln:1-7]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="6-9" {!./docs_src/tutorial/connect/create_tables/tutorial001.py[ln:1-9]!} # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/connect/create_tables/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/connect/create_tables/tutorial001.py!} ``` +//// + /// This is very similar to what we have been doing with the `Hero` model. @@ -88,18 +114,44 @@ Now let's create the `hero` table. This is the same model we have been using up to now, we are just adding the new column `team_id`: +//// tab | Python 3.10+ + +```Python hl_lines="16" +{!./docs_src/tutorial/connect/create_tables/tutorial001_py310.py[ln:1-16]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="18" {!./docs_src/tutorial/connect/create_tables/tutorial001.py[ln:1-18]!} # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/connect/create_tables/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/connect/create_tables/tutorial001.py!} ``` +//// + /// Most of that should look familiar: @@ -134,34 +186,86 @@ You can learn about setting a custom table name for a model in the Advanced User Now we can add the same code as before to create the engine and the function to create the tables: +//// tab | Python 3.10+ + +```Python hl_lines="3-4 6 9-10" +# Code above omitted 👆 + +{!./docs_src/tutorial/connect/create_tables/tutorial001_py310.py[ln:19-26]!} +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3-4 6 9-10" # Code above omitted 👆 {!./docs_src/tutorial/connect/create_tables/tutorial001.py[ln:21-28]!} ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/connect/create_tables/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/connect/create_tables/tutorial001.py!} ``` +//// + /// And as before, we'll call this function from another function `main()`, and we'll add that function `main()` to the main block of the file: +//// tab | Python 3.10+ + +```Python hl_lines="3-4 7-8" +# Code above omitted 👆 + +{!./docs_src/tutorial/connect/create_tables/tutorial001_py310.py[ln:29-34]!} +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3-4 7-8" # Code above omitted 👆 {!./docs_src/tutorial/connect/create_tables/tutorial001.py[ln:31-36]!} ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/connect/create_tables/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/connect/create_tables/tutorial001.py!} ``` +//// + /// ## Run the Code diff --git a/docs/tutorial/connect/read-connected-data.md b/docs/tutorial/connect/read-connected-data.md index 128eb550b7..19cdbe172e 100644 --- a/docs/tutorial/connect/read-connected-data.md +++ b/docs/tutorial/connect/read-connected-data.md @@ -37,10 +37,22 @@ We will continue with the code in the previous example and we will add more thin /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/connect/insert/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/connect/insert/tutorial001.py!} ``` +//// + /// ## `SELECT` Connected Data with SQL @@ -123,6 +135,20 @@ Remember SQLModel's `select()` function? It can take more than one argument. So, we can pass the `Hero` and `Team` model classes. And we can also use both their columns in the `.where()` part: +//// tab | Python 3.10+ + +```Python hl_lines="5" +# Code above omitted 👆 + +{!./docs_src/tutorial/connect/select/tutorial001_py310.py[ln:61-63]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="5" # Code above omitted 👆 @@ -131,12 +157,26 @@ So, we can pass the `Hero` and `Team` model classes. And we can also use both th # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/connect/select/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/connect/select/tutorial001.py!} ``` +//// + /// Notice that in the comparison with `==` we are using the class attributes for both `Hero.team_id` and `Team.id`. @@ -147,6 +187,20 @@ Now we can execute it and get the `results` object. And as we used `select` with two models, we will receive tuples of instances of those two models, so we can iterate over them naturally in a `for` loop: +//// tab | Python 3.10+ + +```Python hl_lines="7" +# Code above omitted 👆 + +{!./docs_src/tutorial/connect/select/tutorial001_py310.py[ln:61-66]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="7" # Code above omitted 👆 @@ -155,12 +209,26 @@ And as we used `select` with two models, we will receive tuples of instances of # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/connect/select/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/connect/select/tutorial001.py!} ``` +//// + /// For each iteration in the `for` loop we get a a tuple with an instance of the class `Hero` and an instance of the class `Team`. @@ -179,6 +247,20 @@ And you should get autocompletion and inline errors in your editor for both `her As always, we must remember to add this new `select_heroes()` function to the `main()` function to make sure it is executed when we call this program from the command line. +//// tab | Python 3.10+ + +```Python hl_lines="6" +# Code above omitted 👆 + +{!./docs_src/tutorial/connect/select/tutorial001_py310.py[ln:69-72]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="6" # Code above omitted 👆 @@ -187,12 +269,26 @@ As always, we must remember to add this new `select_heroes()` function to the `m # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/connect/select/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/connect/select/tutorial001.py!} ``` +//// + /// @@ -300,6 +396,20 @@ The same way there's a `.where()` available when using `select()`, there's also And in SQLModel (actually SQLAlchemy), when using the `.join()`, because we already declared what is the `foreign_key` when creating the models, we don't have to pass an `ON` part, it is inferred automatically: +//// tab | Python 3.10+ + +```Python hl_lines="5" +# Code above omitted 👆 + +{!./docs_src/tutorial/connect/select/tutorial002_py310.py[ln:61-66]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="5" # Code above omitted 👆 @@ -308,12 +418,26 @@ And in SQLModel (actually SQLAlchemy), when using the `.join()`, because we alre # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/connect/select/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/connect/select/tutorial002.py!} ``` +//// + /// Also notice that we are still including `Team` in the `select(Hero, Team)`, because we still want to access that data. @@ -441,6 +565,20 @@ Now let's replicate the same query in **SQLModel**. `.join()` has a parameter we can use `isouter=True` to make the `JOIN` be a `LEFT OUTER JOIN`: +//// tab | Python 3.10+ + +```Python hl_lines="5" +# Code above omitted 👆 + +{!./docs_src/tutorial/connect/select/tutorial003_py310.py[ln:61-66]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="5" # Code above omitted 👆 @@ -449,12 +587,26 @@ Now let's replicate the same query in **SQLModel**. # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/connect/select/tutorial003_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/connect/select/tutorial003.py!} ``` +//// + /// And if we run it, it will output: @@ -502,6 +654,20 @@ But we would still be able to **filter** the rows with it. 🤓 We could even add some additional `.where()` after `.join()` to filter the data more, for example to return only the heroes from one team: +//// tab | Python 3.10+ + +```Python hl_lines="5" +# Code above omitted 👆 + +{!./docs_src/tutorial/connect/select/tutorial004_py310.py[ln:61-66]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="5" # Code above omitted 👆 @@ -510,12 +676,26 @@ We could even add some additional `.where()` after `.join()` to filter the data # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/connect/select/tutorial004_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/connect/select/tutorial004.py!} ``` +//// + /// Here we are **filtering** with `.where()` to get only the heroes that belong to the **Preventers** team. @@ -547,6 +727,20 @@ Preventer Hero: id=2 secret_name='Tommy Sharp' team_id=1 name='Rusty-Man' age=48 By putting the `Team` in `select()` we tell **SQLModel** and the database that we want the team data too. +//// tab | Python 3.10+ + +```Python hl_lines="5" +# Code above omitted 👆 + +{!./docs_src/tutorial/connect/select/tutorial005_py310.py[ln:61-66]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="5" # Code above omitted 👆 @@ -555,12 +749,26 @@ By putting the `Team` in `select()` we tell **SQLModel** and the database that w # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/connect/select/tutorial005_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/connect/select/tutorial005.py!} ``` +//// + /// And if we run that, it will output: diff --git a/docs/tutorial/connect/remove-data-connections.md b/docs/tutorial/connect/remove-data-connections.md index 72e933cb6f..5c2977fb13 100644 --- a/docs/tutorial/connect/remove-data-connections.md +++ b/docs/tutorial/connect/remove-data-connections.md @@ -37,10 +37,22 @@ We will continue with the code from the previous chapter. /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/connect/update/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/connect/update/tutorial001.py!} ``` +//// + /// ## Break a Connection @@ -51,6 +63,24 @@ Let's say **Spider-Boy** is tired of the lack of friendly neighbors and wants to We can simply set the `team_id` to `None`, and now it doesn't have a connection with the team: +//// tab | Python 3.10+ + +```Python hl_lines="8" +# Code above omitted 👆 + +{!./docs_src/tutorial/connect/delete/tutorial001_py310.py[ln:29-30]!} + + # Previous code here omitted 👈 + +{!./docs_src/tutorial/connect/delete/tutorial001_py310.py[ln:66-70]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="8" # Code above omitted 👆 @@ -63,12 +93,26 @@ We can simply set the `team_id` to `None`, and now it doesn't have a connection # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/connect/delete/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/connect/delete/tutorial001.py!} ``` +//// + /// Again, we just **assign** a value to that field attribute `team_id`, now the value is `None`, which means `NULL` in the database. Then we `add()` the hero to the session, and then `commit()`. diff --git a/docs/tutorial/connect/update-data-connections.md b/docs/tutorial/connect/update-data-connections.md index c141aa69d4..8f5687b8ec 100644 --- a/docs/tutorial/connect/update-data-connections.md +++ b/docs/tutorial/connect/update-data-connections.md @@ -39,10 +39,22 @@ We will continue with the code we used to create some heroes, and we'll update t /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/connect/insert/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/connect/insert/tutorial001.py!} ``` +//// + /// ## Assign a Team to a Hero @@ -51,6 +63,24 @@ Let's say that **Tommy Sharp** uses his "rich uncle" charms to recruit **Spider- Doing it is just like updating any other field: +//// tab | Python 3.10+ + +```Python hl_lines="8" +# Code above omitted 👆 + +{!./docs_src/tutorial/connect/update/tutorial001_py310.py[ln:29-30]!} + + # Previous code here omitted 👈 + +{!./docs_src/tutorial/connect/update/tutorial001_py310.py[ln:60-64]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="8" # Code above omitted 👆 @@ -63,12 +93,26 @@ Doing it is just like updating any other field: # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/connect/update/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/connect/update/tutorial001.py!} ``` +//// + /// We can simply **assign** a value to that field attribute `team_id`, then `add()` the hero to the session, and then `commit()`. diff --git a/docs/tutorial/create-db-and-table.md b/docs/tutorial/create-db-and-table.md index 0d8a9a21ce..d87b935a1c 100644 --- a/docs/tutorial/create-db-and-table.md +++ b/docs/tutorial/create-db-and-table.md @@ -41,18 +41,44 @@ That's why this package is called `SQLModel`. Because it's mainly used to create For that, we will import `SQLModel` (plus other things we will also use) and create a class `Hero` that inherits from `SQLModel` and represents the **table model** for our heroes: +//// tab | Python 3.10+ + +```Python hl_lines="1 4" +{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py[ln:1-8]!} + +# More code here later 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3 6" {!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-10]!} # More code here later 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/create_db_and_table/tutorial001.py!} ``` +//// + /// This class `Hero` **represents the table** for our heroes. And each instance we create later will **represent a row** in the table. @@ -75,18 +101,44 @@ The name of each of these variables will be the name of the column in the table. And the type of each of them will also be the type of table column: +//// tab | Python 3.10+ + +```Python hl_lines="1 5-8" +{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py[ln:1-8]!} + +# More code here later 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="1 3 7-10" {!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-10]!} # More code here later 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/create_db_and_table/tutorial001.py!} ``` +//// + /// Let's now see with more detail these field/column declarations. @@ -101,18 +153,44 @@ That is the standard way to declare that something "could be an `int` or `None`" And we also set the default value of `age` to `None`. +//// tab | Python 3.10+ + +```Python hl_lines="8" +{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py[ln:1-8]!} + +# More code here later 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="1 10" {!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-10]!} # More code here later 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/create_db_and_table/tutorial001.py!} ``` +//// + /// /// tip @@ -143,18 +221,44 @@ So, we need to mark `id` as the **primary key**. To do that, we use the special `Field` function from `sqlmodel` and set the argument `primary_key=True`: +//// tab | Python 3.10+ + +```Python hl_lines="1 5" +{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py[ln:1-8]!} + +# More code here later 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3 7" {!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-10]!} # More code here later 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/create_db_and_table/tutorial001.py!} ``` +//// + /// That way, we tell **SQLModel** that this `id` field/column is the primary key of the table. @@ -198,18 +302,44 @@ If you have a server database (for example PostgreSQL or MySQL), the **engine** Creating the **engine** is very simple, just call `create_engine()` with a URL for the database to use: +//// tab | Python 3.10+ + +```Python hl_lines="1 14" +{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py[ln:1-16]!} + +# More code here later 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3 16" -{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-16]!} +{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-18]!} # More code here later 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/create_db_and_table/tutorial001.py!} ``` +//// + /// You should normally have a single **engine** object for your whole application and re-use it everywhere. @@ -234,18 +364,44 @@ SQLite supports a special database that lives all *in memory*. Hence, it's very * `sqlite://` +//// tab | Python 3.10+ + +```Python hl_lines="11-12 14" +{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py[ln:1-16]!} + +# More code here later 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="13-14 16" -{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-19]!} +{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-18]!} # More code here later 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/create_db_and_table/tutorial001.py!} ``` +//// + /// You can read a lot more about all the databases supported by **SQLAlchemy** (and that way supported by **SQLModel**) in the SQLAlchemy documentation. @@ -258,18 +414,44 @@ It will make the engine print all the SQL statements it executes, which can help It is particularly useful for **learning** and **debugging**: +//// tab | Python 3.10+ + +```Python hl_lines="14" +{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py[ln:1-16]!} + +# More code here later 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="16" -{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-16]!} +{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-18]!} # More code here later 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/create_db_and_table/tutorial001.py!} ``` +//// + /// But in production, you would probably want to remove `echo=True`: @@ -296,10 +478,22 @@ And SQLModel's version of `create_engine()` is type annotated internally, so you Now everything is in place to finally create the database and table: +//// tab | Python 3.10+ + +```Python hl_lines="16" +{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="18" {!./docs_src/tutorial/create_db_and_table/tutorial001.py!} ``` +//// + /// tip Creating the engine doesn't create the `database.db` file. @@ -411,10 +605,22 @@ Put the code it in a file `app.py` if you haven't already. /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/create_db_and_table/tutorial001.py!} ``` +//// + /// /// tip @@ -520,18 +726,44 @@ In this example it's just the `SQLModel.metadata.create_all(engine)`. Let's put it in a function `create_db_and_tables()`: +//// tab | Python 3.10+ + +```Python hl_lines="17-18" +{!./docs_src/tutorial/create_db_and_table/tutorial002_py310.py[ln:1-18]!} + +# More code here later 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="19-20" {!./docs_src/tutorial/create_db_and_table/tutorial002.py[ln:1-20]!} # More code here later 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/create_db_and_table/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/create_db_and_table/tutorial002.py!} ``` +//// + /// If `SQLModel.metadata.create_all(engine)` was not in a function and we tried to import something from this module (from this file) in another, it would try to create the database and table **every time** we executed that other file that imported this module. @@ -562,10 +794,22 @@ The word **script** often implies that the code could be run independently and e For that we can use the special variable `__name__` in an `if` block: +//// tab | Python 3.10+ + +```Python hl_lines="21-22" +{!./docs_src/tutorial/create_db_and_table/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="23-24" {!./docs_src/tutorial/create_db_and_table/tutorial002.py!} ``` +//// + ### About `__name__ == "__main__"` The main purpose of the `__name__ == "__main__"` is to have some code that is executed when your file is called with: @@ -658,12 +902,26 @@ But now we can import things from this module in other files. Now, let's give the code a final look: +//// tab | Python 3.10+ + +```{.python .annotate} +{!./docs_src/tutorial/create_db_and_table/tutorial003_py310.py!} +``` + +{!./docs_src/tutorial/create_db_and_table/annotations/en/tutorial003.md!} + +//// + +//// tab | Python 3.7+ + ```{.python .annotate} {!./docs_src/tutorial/create_db_and_table/tutorial003.py!} ``` {!./docs_src/tutorial/create_db_and_table/annotations/en/tutorial003.md!} +//// + /// tip Review what each line does by clicking each number bubble in the code. 👆 diff --git a/docs/tutorial/delete.md b/docs/tutorial/delete.md index 437d388b87..9cb6748b7c 100644 --- a/docs/tutorial/delete.md +++ b/docs/tutorial/delete.md @@ -8,10 +8,22 @@ As before, we'll continue from where we left off with the previous code. /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/update/tutorial003_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/update/tutorial003.py!} ``` +//// + /// Remember to remove the `database.db` file before running the examples to get the same results. @@ -62,6 +74,20 @@ To get the same results, delete the `database.db` file before running the exampl We'll start by selecting the hero `"Spider-Youngster"` that we updated in the previous chapter, this is the one we will delete: +//// tab | Python 3.10+ + +```Python hl_lines="5" +# Code above omitted 👆 + +{!./docs_src/tutorial/delete/tutorial001_py310.py[ln:70-75]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="5" # Code above omitted 👆 @@ -70,28 +96,68 @@ We'll start by selecting the hero `"Spider-Youngster"` that we updated in the pr # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/delete/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/delete/tutorial001.py!} ``` +//// + /// As this is a new function `delete_heroes()`, we'll also add it to the `main()` function so that we call it when executing the program from the command line: +//// tab | Python 3.10+ + +```Python hl_lines="7" +# Code above omitted 👆 + +{!./docs_src/tutorial/delete/tutorial001_py310.py[ln:90-98]!} +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="7" # Code above omitted 👆 {!./docs_src/tutorial/delete/tutorial001.py[ln:92-100]!} ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/delete/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/delete/tutorial001.py!} ``` +//// + /// That will print the same existing hero **Spider-Youngster**: @@ -120,6 +186,20 @@ Hero: name='Spider-Youngster' secret_name='Pedro Parqueador' age=16 id=2 Now, very similar to how we used `session.add()` to add or update new heroes, we can use `session.delete()` to delete the hero from the session: +//// tab | Python 3.10+ + +```Python hl_lines="10" +# Code above omitted 👆 + +{!./docs_src/tutorial/delete/tutorial001_py310.py[ln:70-77]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="10" # Code above omitted 👆 @@ -128,12 +208,26 @@ Now, very similar to how we used `session.add()` to add or update new heroes, we # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/delete/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/delete/tutorial001.py!} ``` +//// + /// ## Commit the Session @@ -142,6 +236,20 @@ To save the current changes in the session, **commit** it. This will save all the changes stored in the **session**, like the deleted hero: +//// tab | Python 3.10+ + +```Python hl_lines="11" +# Code above omitted 👆 + +{!./docs_src/tutorial/delete/tutorial001_py310.py[ln:70-78]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="11" # Code above omitted 👆 @@ -150,12 +258,26 @@ This will save all the changes stored in the **session**, like the deleted hero: # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/delete/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/delete/tutorial001.py!} ``` +//// + /// The same as we have seen before, `.commit()` will also save anything else that was added to the session. Including updates, or created heroes. @@ -191,6 +313,20 @@ As the object is not connected to the session, it is not marked as "expired", th Because of that, the object still contains its attributes with the data in it, so we can print it: +//// tab | Python 3.10+ + +```Python hl_lines="13" +# Code above omitted 👆 + +{!./docs_src/tutorial/delete/tutorial001_py310.py[ln:70-80]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="13" # Code above omitted 👆 @@ -199,12 +335,26 @@ Because of that, the object still contains its attributes with the data in it, s # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/delete/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/delete/tutorial001.py!} ``` +//// + /// This will output: @@ -228,6 +378,20 @@ Deleted hero: name='Spider-Youngster' secret_name='Pedro Parqueador' age=16 id=2 To confirm if it was deleted, now let's query the database again, with the same `"Spider-Youngster"` name: +//// tab | Python 3.10+ + +```Python hl_lines="15-17" +# Code above omitted 👆 + +{!./docs_src/tutorial/delete/tutorial001_py310.py[ln:70-84]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="15-17" # Code above omitted 👆 @@ -236,12 +400,26 @@ To confirm if it was deleted, now let's query the database again, with the same # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/delete/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/delete/tutorial001.py!} ``` +//// + /// Here we are using `results.first()` to get the first object found (in case it found multiple) or `None`, if it didn't find anything. @@ -279,6 +457,20 @@ Now let's just confirm that, indeed, no hero was found in the database with that We'll do it by checking that the "first" item in the `results` is `None`: +//// tab | Python 3.10+ + +```Python hl_lines="19-20" +# Code above omitted 👆 + +{!./docs_src/tutorial/delete/tutorial001_py310.py[ln:70-87]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="19-20" # Code above omitted 👆 @@ -287,12 +479,26 @@ We'll do it by checking that the "first" item in the `results` is `None`: # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/delete/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/delete/tutorial001.py!} ``` +//// + /// This will output: @@ -319,12 +525,26 @@ INFO Engine ROLLBACK Now let's review all that code: +//// tab | Python 3.10+ + +```{ .python .annotate hl_lines="70-88" } +{!./docs_src/tutorial/delete/tutorial002_py310.py!} +``` + +{!./docs_src/tutorial/delete/annotations/en/tutorial002.md!} + +//// + +//// tab | Python 3.7+ + ```{ .python .annotate hl_lines="72-90" } {!./docs_src/tutorial/delete/tutorial002.py!} ``` {!./docs_src/tutorial/delete/annotations/en/tutorial002.md!} +//// + /// tip Check out the number bubbles to see what is done by each line of code. diff --git a/docs/tutorial/fastapi/delete.md b/docs/tutorial/fastapi/delete.md index 02fdc9fb52..87144cc08d 100644 --- a/docs/tutorial/fastapi/delete.md +++ b/docs/tutorial/fastapi/delete.md @@ -12,6 +12,32 @@ We get a `hero_id` from the path parameter and verify if it exists, just as we d And if we actually find a hero, we just delete it with the **session**. +//// tab | Python 3.10+ + +```Python hl_lines="3-11" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/delete/tutorial001_py310.py[ln:89-97]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="3-11" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/delete/tutorial001_py39.py[ln:91-99]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3-11" # Code above omitted 👆 @@ -20,12 +46,34 @@ And if we actually find a hero, we just delete it with the **session**. # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/delete/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/delete/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/delete/tutorial001.py!} ``` +//// + /// After deleting it successfully, we just return a response of: diff --git a/docs/tutorial/fastapi/limit-and-offset.md b/docs/tutorial/fastapi/limit-and-offset.md index b9d1c8b73c..61d282f60c 100644 --- a/docs/tutorial/fastapi/limit-and-offset.md +++ b/docs/tutorial/fastapi/limit-and-offset.md @@ -22,6 +22,36 @@ By default, we will return the first results from the database, so `offset` will And by default, we will return a maximum of `100` heroes, so `limit` will have a default value of `100`. +//// tab | Python 3.10+ + +```Python hl_lines="1 7 9" +{!./docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py310.py[ln:1-2]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py310.py[ln:52-56]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="3 9 11" +{!./docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py39.py[ln:1-4]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py39.py[ln:54-58]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3 9 11" {!./docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py[ln:1-4]!} @@ -32,12 +62,34 @@ And by default, we will return a maximum of `100` heroes, so `limit` will have a # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py!} ``` +//// + /// We want to allow clients to set different `offset` and `limit` values. diff --git a/docs/tutorial/fastapi/multiple-models.md b/docs/tutorial/fastapi/multiple-models.md index 3995daa650..c4db7c6742 100644 --- a/docs/tutorial/fastapi/multiple-models.md +++ b/docs/tutorial/fastapi/multiple-models.md @@ -109,6 +109,36 @@ And we want to have a `HeroRead` with the `id` field, but this time annotated wi The simplest way to solve it could be to create **multiple models**, each one with all the corresponding fields: +//// tab | Python 3.10+ + +```Python hl_lines="5-9 12-15 18-22" +# This would work, but there's a better option below 🚨 + +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py[ln:5-22]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="5-9 12-15 18-22" +# This would work, but there's a better option below 🚨 + +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py[ln:7-24]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="5-9 12-15 18-22" # This would work, but there's a better option below 🚨 @@ -119,12 +149,34 @@ The simplest way to solve it could be to create **multiple models**, each one wi # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/multiple_models/tutorial001.py!} ``` +//// + /// Here's the important detail, and probably the most important feature of **SQLModel**: only `Hero` is declared with `table = True`. @@ -147,6 +199,32 @@ Let's now see how to use these new models in the FastAPI application. Let's first check how is the process to create a hero now: +//// tab | Python 3.10+ + +```Python hl_lines="3-4 6" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py[ln:44-51]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="3-4 6" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py[ln:46-53]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3-4 6" # Code above omitted 👆 @@ -155,18 +233,66 @@ Let's first check how is the process to create a hero now: # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/multiple_models/tutorial001.py!} ``` +//// + /// Let's check that in detail. Now we use the type annotation `HeroCreate` for the request JSON data in the `hero` parameter of the **path operation function**. +//// tab | Python 3.10+ + +```Python hl_lines="3" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py[ln:45]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="3" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py[ln:47]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3" # Code above omitted 👆 @@ -175,6 +301,8 @@ Now we use the type annotation `HeroCreate` for the request JSON data in the `he # Code below omitted 👇 ``` +//// + Then we create a new `Hero` (this is the actual **table** model that saves things to the database) using `Hero.model_validate()`. The method `.model_validate()` reads data from another object with attributes (or a dict) and creates a new instance of this class, in this case `Hero`. @@ -187,6 +315,32 @@ In versions of **SQLModel** before `0.0.14` you would use the method `.from_orm( We can now create a new `Hero` instance (the one for the database) and put it in the variable `db_hero` from the data in the `hero` variable that is the `HeroCreate` instance we received from the request. +//// tab | Python 3.10+ + +```Python hl_lines="3" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py[ln:47]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="3" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py[ln:49]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3" # Code above omitted 👆 @@ -195,12 +349,40 @@ We can now create a new `Hero` instance (the one for the database) and put it in # Code below omitted 👇 ``` +//// + Then we just `add` it to the **session**, `commit`, and `refresh` it, and finally, we return the same `db_hero` variable that has the just refreshed `Hero` instance. Because it is just refreshed, it has the `id` field set with a new ID taken from the database. And now that we return it, FastAPI will validate the data with the `response_model`, which is a `HeroRead`: +//// tab | Python 3.10+ + +```Python hl_lines="3" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py[ln:44]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="3" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py[ln:46]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3" # Code above omitted 👆 @@ -209,6 +391,8 @@ And now that we return it, FastAPI will validate the data with the `response_mod # Code below omitted 👇 ``` +//// + This will validate that all the data that we promised is there and will remove any data we didn't declare. /// tip @@ -259,6 +443,32 @@ We can see from above that they all share some **base** fields: So let's create a **base** model `HeroBase` that the others can inherit from: +//// tab | Python 3.10+ + +```Python hl_lines="3-6" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py[ln:5-8]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="3-6" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py[ln:7-10]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3-6" # Code above omitted 👆 @@ -267,12 +477,34 @@ So let's create a **base** model `HeroBase` that the others can inherit from: # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py!} ``` +//// + /// As you can see, this is *not* a **table model**, it doesn't have the `table = True` config. @@ -283,6 +515,32 @@ But now we can create the **other models inheriting from it**, they will all sha Let's start with the only **table model**, the `Hero`: +//// tab | Python 3.10+ + +```Python hl_lines="9-10" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py[ln:5-12]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="9-10" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py[ln:7-14]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="9-10" # Code above omitted 👆 @@ -291,12 +549,34 @@ Let's start with the only **table model**, the `Hero`: # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py!} ``` +//// + /// Notice that `Hero` now doesn't inherit from `SQLModel`, but from `HeroBase`. @@ -313,6 +593,32 @@ And those inherited fields will also be in the **autocompletion** and **inline e Notice that the parent model `HeroBase` is not a **table model**, but still, we can declare `name` and `age` using `Field(index=True)`. +//// tab | Python 3.10+ + +```Python hl_lines="4 6 9" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py[ln:5-12]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="4 6 9" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py[ln:7-14]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="4 6 9" # Code above omitted 👆 @@ -321,12 +627,34 @@ Notice that the parent model `HeroBase` is not a **table model**, but still, we # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py!} ``` +//// + /// This won't affect this parent **data model** `HeroBase`. @@ -339,6 +667,32 @@ Now let's see the `HeroCreate` model that will be used to define the data that w This is a fun one: +//// tab | Python 3.10+ + +```Python hl_lines="13-14" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py[ln:5-16]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="13-14" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py[ln:7-18]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="13-14" # Code above omitted 👆 @@ -347,12 +701,34 @@ This is a fun one: # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py!} ``` +//// + /// What's happening here? @@ -373,6 +749,32 @@ Now let's check the `HeroRead` model. This one just declares that the `id` field is required when reading a hero from the API, because a hero read from the API will come from the database, and in the database it will always have an ID. +//// tab | Python 3.10+ + +```Python hl_lines="17-18" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py[ln:5-20]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="17-18" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py[ln:7-22]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="17-18" # Code above omitted 👆 @@ -381,12 +783,34 @@ This one just declares that the `id` field is required when reading a hero from # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py!} ``` +//// + /// ## Review the Updated Docs UI diff --git a/docs/tutorial/fastapi/read-one.md b/docs/tutorial/fastapi/read-one.md index 0fab696c19..becb2c6c6c 100644 --- a/docs/tutorial/fastapi/read-one.md +++ b/docs/tutorial/fastapi/read-one.md @@ -14,6 +14,32 @@ If you need to refresh how *path parameters* work, including their data validati /// +//// tab | Python 3.10+ + +```Python hl_lines="6" +{!./docs_src/tutorial/fastapi/read_one/tutorial001_py310.py[ln:1-2]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/read_one/tutorial001_py310.py[ln:59-65]!} +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="8" +{!./docs_src/tutorial/fastapi/read_one/tutorial001_py39.py[ln:1-4]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/read_one/tutorial001_py39.py[ln:61-67]!} +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="8" {!./docs_src/tutorial/fastapi/read_one/tutorial001.py[ln:1-4]!} @@ -22,12 +48,34 @@ If you need to refresh how *path parameters* work, including their data validati {!./docs_src/tutorial/fastapi/read_one/tutorial001.py[ln:61-67]!} ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/read_one/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/read_one/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/read_one/tutorial001.py!} ``` +//// + /// For example, to get the hero with ID `2` we would send a `GET` request to: @@ -48,6 +96,32 @@ And to use it, we first import `HTTPException` from `fastapi`. This will let the client know that they probably made a mistake on their side and requested a hero that doesn't exist in the database. +//// tab | Python 3.10+ + +```Python hl_lines="1 9-11" +{!./docs_src/tutorial/fastapi/read_one/tutorial001_py310.py[ln:1-2]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/read_one/tutorial001_py310.py[ln:59-65]!} +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="3 11-13" +{!./docs_src/tutorial/fastapi/read_one/tutorial001_py39.py[ln:1-4]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/read_one/tutorial001_py39.py[ln:61-67]!} +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3 11-13" {!./docs_src/tutorial/fastapi/read_one/tutorial001.py[ln:1-4]!} @@ -56,12 +130,34 @@ This will let the client know that they probably made a mistake on their side an {!./docs_src/tutorial/fastapi/read_one/tutorial001.py[ln:61-67]!} ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/read_one/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/read_one/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/read_one/tutorial001.py!} ``` +//// + /// ## Return the Hero @@ -70,6 +166,32 @@ Then, if the hero exists, we return it. And because we are using the `response_model` with `HeroRead`, it will be validated, documented, etc. +//// tab | Python 3.10+ + +```Python hl_lines="6 12" +{!./docs_src/tutorial/fastapi/read_one/tutorial001_py310.py[ln:1-2]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/read_one/tutorial001_py310.py[ln:59-65]!} +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="8 14" +{!./docs_src/tutorial/fastapi/read_one/tutorial001_py39.py[ln:1-4]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/read_one/tutorial001_py39.py[ln:61-67]!} +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="8 14" {!./docs_src/tutorial/fastapi/read_one/tutorial001.py[ln:1-4]!} @@ -78,12 +200,34 @@ And because we are using the `response_model` with `HeroRead`, it will be valida {!./docs_src/tutorial/fastapi/read_one/tutorial001.py[ln:61-67]!} ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/read_one/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/read_one/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/read_one/tutorial001.py!} ``` +//// + /// ## Check the Docs UI diff --git a/docs/tutorial/fastapi/relationships.md b/docs/tutorial/fastapi/relationships.md index 3275dcfa17..e2a2678903 100644 --- a/docs/tutorial/fastapi/relationships.md +++ b/docs/tutorial/fastapi/relationships.md @@ -44,6 +44,56 @@ It's because we declared the `HeroRead` with only the same base fields of the `H And the same way, we declared the `TeamRead` with only the same base fields of the `TeamBase` plus the `id`. But it doesn't include a field `heroes` for the **relationship attribute**. +//// tab | Python 3.10+ + +```Python hl_lines="3-5 9-10 14-19 23-24" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py[ln:5-7]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py[ln:20-21]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py[ln:29-34]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py[ln:43-44]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="3-5 9-10 14-19 23-24" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py[ln:7-9]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py[ln:22-23]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py[ln:31-36]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py[ln:45-46]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3-5 9-10 14-19 23-24" # Code above omitted 👆 @@ -64,18 +114,74 @@ And the same way, we declared the `TeamRead` with only the same base fields of t # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/teams/tutorial001.py!} ``` +//// + /// Now, remember that FastAPI uses the `response_model` to validate and **filter** the response data? In this case, we used `response_model=TeamRead` and `response_model=HeroRead`, so FastAPI will use them to filter the response data, even if we return a **table model** that includes **relationship attributes**: +//// tab | Python 3.10+ + +```Python hl_lines="3 8 12 17" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py[ln:102-107]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py[ln:156-161]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="3 8 12 17" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py[ln:104-109]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py[ln:158-163]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3 8 12 17" # Code above omitted 👆 @@ -88,12 +194,34 @@ In this case, we used `response_model=TeamRead` and `response_model=HeroRead`, s # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/teams/tutorial001.py!} ``` +//// + /// ## Don't Include All the Data @@ -176,6 +304,32 @@ Let's add the models `HeroReadWithTeam` and `TeamReadWithHeroes`. We'll add them **after** the other models so that we can easily reference the previous models. +//// tab | Python 3.10+ + +```Python hl_lines="3-4 7-8" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/relationships/tutorial001_py310.py[ln:59-64]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="3-4 7-8" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/relationships/tutorial001_py39.py[ln:61-66]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3-4 7-8" # Code above omitted 👆 @@ -184,12 +338,34 @@ We'll add them **after** the other models so that we can easily reference the pr # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/relationships/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/relationships/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/relationships/tutorial001.py!} ``` +//// + /// These two models are very **simple in code**, but there's a lot happening here. Let's check it out. @@ -224,6 +400,40 @@ This will tell **FastAPI** to take the object that we return from the *path oper In the case of the hero, this tells FastAPI to extract the `team` too. And in the case of the team, to extract the list of `heroes` too. +//// tab | Python 3.10+ + +```Python hl_lines="3 8 12 17" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/relationships/tutorial001_py310.py[ln:111-116]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/relationships/tutorial001_py310.py[ln:165-170]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="3 8 12 17" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/relationships/tutorial001_py39.py[ln:113-118]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/relationships/tutorial001_py39.py[ln:167-172]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3 8 12 17" # Code above omitted 👆 @@ -236,12 +446,34 @@ In the case of the hero, this tells FastAPI to extract the `team` too. And in th # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/relationships/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/relationships/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/relationships/tutorial001.py!} ``` +//// + /// ## Check It Out in the Docs UI diff --git a/docs/tutorial/fastapi/response-model.md b/docs/tutorial/fastapi/response-model.md index f6e20b3354..b333d58b1a 100644 --- a/docs/tutorial/fastapi/response-model.md +++ b/docs/tutorial/fastapi/response-model.md @@ -32,6 +32,32 @@ We can use `response_model` to tell FastAPI the schema of the data we want to se For example, we can pass the same `Hero` **SQLModel** class (because it is also a Pydantic model): +//// tab | Python 3.10+ + +```Python hl_lines="3" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/response_model/tutorial001_py310.py[ln:31-37]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="3" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/response_model/tutorial001_py39.py[ln:33-39]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3" # Code above omitted 👆 @@ -40,12 +66,34 @@ For example, we can pass the same `Hero` **SQLModel** class (because it is also # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/response_model/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/response_model/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/response_model/tutorial001.py!} ``` +//// + /// ## List of Heroes in `response_model` @@ -54,6 +102,34 @@ We can also use other type annotations, the same way we can use with Pydantic fi First, we import `List` from `typing` and then we declare the `response_model` with `List[Hero]`: +//// tab | Python 3.10+ + +```Python hl_lines="3" + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/response_model/tutorial001_py310.py[ln:40-44]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="3" + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/response_model/tutorial001_py39.py[ln:42-46]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="1 5" {!./docs_src/tutorial/fastapi/response_model/tutorial001.py[ln:1]!} @@ -64,12 +140,34 @@ First, we import `List` from `typing` and then we declare the `response_model` w # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/response_model/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/response_model/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/response_model/tutorial001.py!} ``` +//// + /// ## FastAPI and Response Model diff --git a/docs/tutorial/fastapi/session-with-dependency.md b/docs/tutorial/fastapi/session-with-dependency.md index ce2421fa82..e148452dcb 100644 --- a/docs/tutorial/fastapi/session-with-dependency.md +++ b/docs/tutorial/fastapi/session-with-dependency.md @@ -6,6 +6,32 @@ Before we keep adding things, let's change a bit how we get the session for each Up to now, we have been creating a session in each *path operation*, in a `with` block. +//// tab | Python 3.10+ + +```Python hl_lines="5" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/delete/tutorial001_py310.py[ln:48-55]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="5" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/delete/tutorial001_py39.py[ln:50-57]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="5" # Code above omitted 👆 @@ -14,12 +40,34 @@ Up to now, we have been creating a session in each *path operation*, in a `with` # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/delete/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/delete/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/delete/tutorial001.py!} ``` +//// + /// That's perfectly fine, but in many use cases we would want to use FastAPI Dependencies, for example to **verify** that the client is **logged in** and get the **current user** before executing any other code in the *path operation*. @@ -34,6 +82,32 @@ A **FastAPI** dependency is very simple, it's just a function that returns a val It could use `yield` instead of `return`, and in that case **FastAPI** will make sure it executes all the code **after** the `yield`, once it is done with the request. +//// tab | Python 3.10+ + +```Python hl_lines="3-5" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:40-42]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="3-5" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:42-44]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3-5" # Code above omitted 👆 @@ -42,12 +116,34 @@ It could use `yield` instead of `return`, and in that case **FastAPI** will make # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py!} ``` +//// + /// ## Use the Dependency @@ -56,6 +152,44 @@ Now let's make FastAPI execute a dependency and get its value in the *path opera We import `Depends()` from `fastapi`. Then we use it in the *path operation function* in a **parameter**, the same way we declared parameters to get JSON bodies, path parameters, etc. +//// tab | Python 3.10+ + +```Python hl_lines="1 13" +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:1-2]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:40-42]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:53-59]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="3 15" +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:1-4]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:42-44]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:55-61]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3 15" {!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:1-4]!} @@ -70,12 +204,34 @@ We import `Depends()` from `fastapi`. Then we use it in the *path operation func # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py!} ``` +//// + /// /// tip @@ -104,6 +260,44 @@ And because dependencies can use `yield`, FastAPI will make sure to run the code This means that in the main code of the *path operation function*, it will work equivalently to the previous version with the explicit `with` block. +//// tab | Python 3.10+ + +```Python hl_lines="14-18" +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:1-2]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:40-42]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:53-59]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="16-20" +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:1-4]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:42-44]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:55-61]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="16-20" {!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:1-4]!} @@ -118,18 +312,78 @@ This means that in the main code of the *path operation function*, it will work # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py!} ``` +//// + /// In fact, you could think that all that block of code inside of the `create_hero()` function is still inside a `with` block for the **session**, because this is more or less what's happening behind the scenes. But now, the `with` block is not explicitly in the function, but in the dependency above: +//// tab | Python 3.10+ + +```Python hl_lines="7-8" +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:1-2]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:40-42]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:53-59]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="9-10" +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:1-4]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:42-44]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:55-61]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="9-10" {!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:1-4]!} @@ -144,12 +398,34 @@ But now, the `with` block is not explicitly in the function, but in the dependen # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py!} ``` +//// + /// We will see how this is very useful when testing the code later. ✅ @@ -166,6 +442,40 @@ session: Session = Depends(get_session) And then we remove the previous `with` block with the old **session**. +//// tab | Python 3.10+ + +```Python hl_lines="13 24 33 42 57" +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:1-2]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:40-42]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:53-104]!} +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="15 26 35 44 59" +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:1-4]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:42-44]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:55-106]!} +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="15 26 35 44 59" {!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:1-4]!} @@ -178,12 +488,34 @@ And then we remove the previous `with` block with the old **session**. {!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:55-106]!} ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py!} ``` +//// + /// ## Recap diff --git a/docs/tutorial/fastapi/simple-hero-api.md b/docs/tutorial/fastapi/simple-hero-api.md index 2ef8b436d0..0a96118953 100644 --- a/docs/tutorial/fastapi/simple-hero-api.md +++ b/docs/tutorial/fastapi/simple-hero-api.md @@ -32,6 +32,22 @@ We will start with the **simplest version**, with just heroes (no teams yet). This is almost the same code we have seen up to now in previous examples: +//// tab | Python 3.10+ + +```Python hl_lines="18-19" + +# One line of FastAPI imports here later 👈 +{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py[ln:2]!} + +{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py[ln:5-20]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="20-21" {!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py[ln:1]!} @@ -43,12 +59,26 @@ This is almost the same code we have seen up to now in previous examples: # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py!} ``` +//// + /// There's only one change here from the code we have used before, the `check_same_thread` in the `connect_args`. @@ -77,6 +107,22 @@ We will import the `FastAPI` class from `fastapi`. And then create an `app` object that is an instance of that `FastAPI` class: +//// tab | Python 3.10+ + +```Python hl_lines="1 6" +{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py[ln:1-2]!} + +# SQLModel code here omitted 👈 + +{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py[ln:23]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3 8" {!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py[ln:1-4]!} @@ -87,12 +133,26 @@ And then create an `app` object that is an instance of that `FastAPI` class: # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py!} ``` +//// + /// ## Create Database and Tables on `startup` @@ -101,6 +161,20 @@ We want to make sure that once the app starts running, the function `create_tabl This should be called only once at startup, not before every request, so we put it in the function to handle the `"startup"` event: +//// tab | Python 3.10+ + +```Python hl_lines="6-8" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py[ln:23-28]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="6-8" # Code above omitted 👆 @@ -109,12 +183,26 @@ This should be called only once at startup, not before every request, so we put # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py!} ``` +//// + /// ## Create Heroes *Path Operation* @@ -129,6 +217,20 @@ Let's create the **path operation** code to create a new hero. It will be called when a user sends a request with a `POST` **operation** to the `/heroes/` **path**: +//// tab | Python 3.10+ + +```Python hl_lines="11-12" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py[ln:23-37]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="11-12" # Code above omitted 👆 @@ -137,12 +239,26 @@ It will be called when a user sends a request with a `POST` **operation** to the # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py!} ``` +//// + /// /// info @@ -177,18 +293,44 @@ We will improve this further later, but for now, it already shows the power of h Now let's add another **path operation** to read all the heroes: +//// tab | Python 3.10+ + +```Python hl_lines="20-24" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py[ln:23-44]!} +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="20-24" # Code above omitted 👆 {!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py[ln:25-46]!} ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py!} ``` +//// + /// This is pretty straightforward. diff --git a/docs/tutorial/fastapi/teams.md b/docs/tutorial/fastapi/teams.md index 0180b9476b..cbdb4a94ad 100644 --- a/docs/tutorial/fastapi/teams.md +++ b/docs/tutorial/fastapi/teams.md @@ -18,18 +18,62 @@ Then we also inherit from the `TeamBase` for the `TeamCreate` and `TeamRead` **d And we also create a `TeamUpdate` **data model**. +//// tab | Python 3.10+ + +```Python hl_lines="5-7 10-13 16-17 20-21 24-26" +{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py[ln:1-26]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="7-9 12-15 18-19 22-23 26-28" +{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py[ln:1-28]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="7-9 12-15 18-19 22-23 26-28" {!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:1-28]!} # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/teams/tutorial001.py!} ``` +//// + /// We now also have **relationship attributes**. 🎉 @@ -38,7 +82,33 @@ Let's now update the `Hero` models too. ## Update Hero Models -```Python hl_lines="3-8 11-15 17-18 21-22 25-29" +//// tab | Python 3.10+ + +```Python hl_lines="3-8 11-14 17-18 21-22 25-29" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py[ln:29-55]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="3-8 11-14 17-18 21-22 25-29" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py[ln:31-57]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + +```Python hl_lines="3-8 11-14 17-18 21-22 25-29" # Code above omitted 👆 {!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:31-57]!} @@ -46,12 +116,34 @@ Let's now update the `Hero` models too. # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/teams/tutorial001.py!} ``` +//// + /// We now have a `team_id` in the hero models. @@ -64,6 +156,32 @@ And even though the `HeroBase` is *not* a **table model**, we can declare `team_ Notice that the **relationship attributes**, the ones with `Relationship()`, are **only** in the **table models**, as those are the ones that are handled by **SQLModel** with SQLAlchemy and that can have the automatic fetching of data from the database when we access them. +//// tab | Python 3.10+ + +```Python hl_lines="11 38" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py[ln:5-55]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="11 38" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py[ln:7-57]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="11 38" # Code above omitted 👆 @@ -72,12 +190,34 @@ Notice that the **relationship attributes**, the ones with `Relationship()`, are # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/teams/tutorial001.py!} ``` +//// + /// ## Path Operations for Teams @@ -86,6 +226,32 @@ Let's now add the **path operations** for teams. These are equivalent and very similar to the **path operations** for the **heroes** we had before, so we don't have to go over the details for each one, let's check the code. +//// tab | Python 3.10+ + +```Python hl_lines="3-9 12-20 23-28 31-47 50-57" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py[ln:136-190]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="3-9 12-20 23-28 31-47 50-57" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py[ln:138-192]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3-9 12-20 23-28 31-47 50-57" # Code above omitted 👆 @@ -94,12 +260,34 @@ These are equivalent and very similar to the **path operations** for the **heroe # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/teams/tutorial001.py!} ``` +//// + /// ## Using Relationships Attributes diff --git a/docs/tutorial/fastapi/update-extra-data.md b/docs/tutorial/fastapi/update-extra-data.md index 71d9b9cefa..6bbd72ad33 100644 --- a/docs/tutorial/fastapi/update-extra-data.md +++ b/docs/tutorial/fastapi/update-extra-data.md @@ -38,6 +38,32 @@ The `Hero` table model will now store a new field `hashed_password`. And the data models for `HeroCreate` and `HeroUpdate` will also have a new field `password` that will contain the plain text password sent by clients. +//// tab | Python 3.10+ + +```Python hl_lines="11 15 26" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/update/tutorial002_py310.py[ln:5-28]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="11 15 26" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/update/tutorial002_py39.py[ln:7-30]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="11 15 26" # Code above omitted 👆 @@ -46,12 +72,34 @@ And the data models for `HeroCreate` and `HeroUpdate` will also have a new field # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/update/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/update/tutorial002_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/update/tutorial002.py!} ``` +//// + /// When a client is creating a new hero, they will send the `password` in the request body. @@ -64,6 +112,40 @@ The app will receive the data from the client using the `HeroCreate` model. This contains the `password` field with the plain text password, and we cannot use that one. So we need to generate a hash from it. +//// tab | Python 3.10+ + +```Python hl_lines="11" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/update/tutorial002_py310.py[ln:42-44]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/update/tutorial002_py310.py[ln:55-57]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="11" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/update/tutorial002_py39.py[ln:44-46]!} + +# Code here omitted 👈 + +{!./docs_src/tutorial/fastapi/update/tutorial002_py39.py[ln:57-59]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="11" # Code above omitted 👆 @@ -76,12 +158,34 @@ This contains the `password` field with the plain text password, and we cannot u # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/update/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/update/tutorial002_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/update/tutorial002.py!} ``` +//// + /// ## Create an Object with Extra Data @@ -138,6 +242,32 @@ So now, `db_user_dict` has the updated `age` field with `32` instead of `None` a Similar to how dictionaries have an `update` method, **SQLModel** models have a parameter `update` in `Hero.model_validate()` that takes a dictionary with extra data, or data that should take precedence: +//// tab | Python 3.10+ + +```Python hl_lines="8" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/update/tutorial002_py310.py[ln:55-64]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="8" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/update/tutorial002_py39.py[ln:57-66]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="8" # Code above omitted 👆 @@ -146,12 +276,34 @@ Similar to how dictionaries have an `update` method, **SQLModel** models have a # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/update/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/update/tutorial002_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/update/tutorial002.py!} ``` +//// + /// Now, `db_hero` (which is a *table model* `Hero`) will extract its values from `hero` (which is a *data model* `HeroCreate`), and then it will **`update`** its values with the extra data from the dictionary `extra_data`. @@ -166,6 +318,32 @@ Now let's say we want to **update a hero** that already exists in the database. The same way as before, to avoid removing existing data, we will use `exclude_unset=True` when calling `hero.model_dump()`, to get a dictionary with only the data sent by the client. +//// tab | Python 3.10+ + +```Python hl_lines="9" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/update/tutorial002_py310.py[ln:83-89]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="9" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/update/tutorial002_py39.py[ln:85-91]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="9" # Code above omitted 👆 @@ -174,12 +352,34 @@ The same way as before, to avoid removing existing data, we will use `exclude_un # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/update/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/update/tutorial002_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/update/tutorial002.py!} ``` +//// + /// Now, this `hero_data` dictionary could contain a `password`. We need to check it, and if it's there, we need to generate the `hashed_password`. @@ -190,6 +390,32 @@ And then we can update the `db_hero` object using the method `db_hero.sqlmodel_u It takes a model object or dictionary with the data to update the object and also an **additional `update` argument** with extra data. +//// tab | Python 3.10+ + +```Python hl_lines="15" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/update/tutorial002_py310.py[ln:83-99]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="15" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/update/tutorial002_py39.py[ln:85-101]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="15" # Code above omitted 👆 @@ -198,12 +424,34 @@ It takes a model object or dictionary with the data to update the object and als # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/update/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/update/tutorial002_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/update/tutorial002.py!} ``` +//// + /// /// tip diff --git a/docs/tutorial/fastapi/update.md b/docs/tutorial/fastapi/update.md index be4d90df16..6b2411bea8 100644 --- a/docs/tutorial/fastapi/update.md +++ b/docs/tutorial/fastapi/update.md @@ -22,6 +22,32 @@ Because each field is **actually different** (we just change it to `Optional`, b So, let's create this new `HeroUpdate` model: +//// tab | Python 3.10+ + +```Python hl_lines="21-24" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/update/tutorial001_py310.py[ln:5-26]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="21-24" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/update/tutorial001_py39.py[ln:7-28]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="21-24" # Code above omitted 👆 @@ -30,12 +56,34 @@ So, let's create this new `HeroUpdate` model: # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/update/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/update/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/update/tutorial001.py!} ``` +//// + /// This is almost the same as `HeroBase`, but all the fields are optional, so we can't simply inherit from `HeroBase`. @@ -46,6 +94,32 @@ Now let's use this model in the *path operation* to update a hero. We will use a `PATCH` HTTP operation. This is used to **partially update data**, which is what we are doing. +//// tab | Python 3.10+ + +```Python hl_lines="3-4" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/update/tutorial001_py310.py[ln:74-89]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="3-4" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/update/tutorial001_py39.py[ln:76-91]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3-4" # Code above omitted 👆 @@ -54,12 +128,34 @@ We will use a `PATCH` HTTP operation. This is used to **partially update data**, # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/update/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/update/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/update/tutorial001.py!} ``` +//// + /// We also read the `hero_id` from the *path parameter* and the request body, a `HeroUpdate`. @@ -70,6 +166,32 @@ We take a `hero_id` with the **ID** of the hero **we want to update**. So, we need to read the hero from the database, with the **same logic** we used to **read a single hero**, checking if it exists, possibly raising an error for the client if it doesn't exist, etc. +//// tab | Python 3.10+ + +```Python hl_lines="6-8" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/update/tutorial001_py310.py[ln:74-89]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="6-8" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/update/tutorial001_py39.py[ln:76-91]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="6-8" # Code above omitted 👆 @@ -78,12 +200,34 @@ So, we need to read the hero from the database, with the **same logic** we used # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/update/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/update/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/update/tutorial001.py!} ``` +//// + /// ### Get the New Data @@ -136,6 +280,32 @@ Then the dictionary we would get in Python using `hero.model_dump(exclude_unset= Then we use that to get the data that was actually sent by the client: +//// tab | Python 3.10+ + +```Python hl_lines="9" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/update/tutorial001_py310.py[ln:74-89]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="9" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/update/tutorial001_py39.py[ln:76-91]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="9" # Code above omitted 👆 @@ -144,12 +314,34 @@ Then we use that to get the data that was actually sent by the client: # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/update/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/update/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/update/tutorial001.py!} ``` +//// + /// /// tip @@ -160,6 +352,32 @@ Before SQLModel 0.0.14, the method was called `hero.dict(exclude_unset=True)`, b Now that we have a **dictionary with the data sent by the client**, we can use the method `db_hero.sqlmodel_update()` to update the object `db_hero`. +//// tab | Python 3.10+ + +```Python hl_lines="10" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/update/tutorial001_py310.py[ln:74-89]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="10" +# Code above omitted 👆 + +{!./docs_src/tutorial/fastapi/update/tutorial001_py39.py[ln:76-91]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="10" # Code above omitted 👆 @@ -168,12 +386,34 @@ Now that we have a **dictionary with the data sent by the client**, we can use t # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/fastapi/update/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/fastapi/update/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/fastapi/update/tutorial001.py!} ``` +//// + /// /// tip diff --git a/docs/tutorial/indexes.md b/docs/tutorial/indexes.md index a7c6028e88..d0854720cf 100644 --- a/docs/tutorial/indexes.md +++ b/docs/tutorial/indexes.md @@ -22,10 +22,22 @@ Fine, in that case, you can **sneak peek** the final code to create indexes here /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python hl_lines="8 10" +{!./docs_src/tutorial/indexes/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="8 10" {!./docs_src/tutorial/indexes/tutorial002.py!} ``` +//// + /// ..but if you are not an expert, **continue reading**, this will probably be useful. 🤓 @@ -263,34 +275,86 @@ The change in code is underwhelming, it's very simple. 😆 Here's the `Hero` model we had before: +//// tab | Python 3.10+ + +```Python hl_lines="6" +{!./docs_src/tutorial/where/tutorial001_py310.py[ln:1-8]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="8" {!./docs_src/tutorial/where/tutorial001.py[ln:1-10]!} # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/where/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/where/tutorial001.py!} ``` +//// + /// Let's now update it to tell **SQLModel** to create an index for the `name` field when creating the table: +//// tab | Python 3.10+ + +```Python hl_lines="6" +{!./docs_src/tutorial/indexes/tutorial001_py310.py[ln:1-8]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="8" {!./docs_src/tutorial/indexes/tutorial001.py[ln:1-10]!} # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/indexes/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/indexes/tutorial001.py!} ``` +//// + /// We use the same `Field()` again as we did before, and set `index=True`. That's it! 🚀 @@ -313,6 +377,20 @@ The SQL database will figure it out **automatically**. ✨ This is great because it means that indexes are very **simple to use**. But it might also feel counterintuitive at first, as you are **not doing anything** explicitly in the code to make it obvious that the index is useful, it all happens in the database behind the scenes. +//// tab | Python 3.10+ + +```Python hl_lines="5" +# Code above omitted 👆 + +{!./docs_src/tutorial/indexes/tutorial001_py310.py[ln:34-39]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="5" # Code above omitted 👆 @@ -321,12 +399,26 @@ This is great because it means that indexes are very **simple to use**. But it m # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/indexes/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/indexes/tutorial001.py!} ``` +//// + /// This is exactly the same code as we had before, but now the database will **use the index** underneath. @@ -370,18 +462,44 @@ secret_name='Dive Wilson' age=None id=1 name='Deadpond' We are going to query the `hero` table doing comparisons on the `age` field too, so we should **define an index** for that one as well: +//// tab | Python 3.10+ + +```Python hl_lines="8" +{!./docs_src/tutorial/indexes/tutorial002_py310.py[ln:1-8]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="10" {!./docs_src/tutorial/indexes/tutorial002.py[ln:1-10]!} # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/indexes/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/indexes/tutorial002.py!} ``` +//// + /// In this case, we want the default value of `age` to continue being `None`, so we set `default=None` when using `Field()`. diff --git a/docs/tutorial/insert.md b/docs/tutorial/insert.md index 9b01db339e..9439cc3774 100644 --- a/docs/tutorial/insert.md +++ b/docs/tutorial/insert.md @@ -25,6 +25,22 @@ We will continue from where we left of in the last chapter. This is the code we had to create the database and table, nothing new here: +//// tab | Python 3.10+ + +```{.python .annotate hl_lines="20" } +{!./docs_src/tutorial/create_db_and_table/tutorial003_py310.py[ln:1-18]!} + +# More code here later 👈 + +{!./docs_src/tutorial/create_db_and_table/tutorial003_py310.py[ln:21-22]!} +``` + +{!./docs_src/tutorial/create_db_and_table/annotations/en/tutorial003.md!} + +//// + +//// tab | Python 3.7+ + ```{.python .annotate hl_lines="22" } {!./docs_src/tutorial/create_db_and_table/tutorial003.py[ln:1-20]!} @@ -35,6 +51,8 @@ This is the code we had to create the database and table, nothing new here: {!./docs_src/tutorial/create_db_and_table/annotations/en/tutorial003.md!} +//// + Now that we can create the database and the table, we will continue from this point and add more code on the same file to create the data. ## Create Data with SQL @@ -127,6 +145,20 @@ So, the first step is to simply create an instance of `Hero`. We'll create 3 right away, for the 3 heroes: +//// tab | Python 3.10+ + +```Python +# Code above omitted 👆 + +{!./docs_src/tutorial/insert/tutorial002_py310.py[ln:21-24]!} + +# More code here later 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python # Code above omitted 👆 @@ -135,12 +167,26 @@ We'll create 3 right away, for the 3 heroes: # More code here later 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/insert/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/insert/tutorial002.py!} ``` +//// + /// /// tip @@ -173,22 +219,62 @@ We would re-use the same **engine** in all the code, everywhere in the applicati The first step is to import the `Session` class: +//// tab | Python 3.10+ + +```Python hl_lines="1" +{!./docs_src/tutorial/insert/tutorial001_py310.py[ln:1]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3" {!./docs_src/tutorial/insert/tutorial001.py[ln:1-3]!} # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/insert/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/insert/tutorial001.py!} ``` +//// + /// Then we can create a new session: +//// tab | Python 3.10+ + +```Python hl_lines="8" +# Code above omitted 👆 + +{!./docs_src/tutorial/insert/tutorial001_py310.py[ln:21-26]!} + +# More code here later 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="8" # Code above omitted 👆 @@ -197,12 +283,26 @@ Then we can create a new session: # More code here later 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/insert/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/insert/tutorial001.py!} ``` +//// + /// The new `Session` takes an `engine` as a parameter. And it will use the **engine** underneath. @@ -217,6 +317,19 @@ We will see a better way to create a **session** using a `with` block later. Now that we have some hero model instances (some objects in memory) and a **session**, the next step is to add them to the session: +//// tab | Python 3.10+ + +```Python hl_lines="9-11" +# Code above omitted 👆 +{!./docs_src/tutorial/insert/tutorial001_py310.py[ln:21-30]!} + +# More code here later 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="9-11" # Code above omitted 👆 {!./docs_src/tutorial/insert/tutorial001.py[ln:23-32]!} @@ -224,12 +337,26 @@ Now that we have some hero model instances (some objects in memory) and a **sess # More code here later 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/insert/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/insert/tutorial001.py!} ``` +//// + /// By this point, our heroes are *not* stored in the database yet. @@ -254,6 +381,19 @@ This ensures that the data is saved in a single batch, and that it will all succ Now that we have the heroes in the **session** and that we are ready to save all that to the database, we can **commit** the changes: +//// tab | Python 3.10+ + +```Python hl_lines="13" +# Code above omitted 👆 +{!./docs_src/tutorial/insert/tutorial001_py310.py[ln:21-32]!} + +# More code here later 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="13" # Code above omitted 👆 {!./docs_src/tutorial/insert/tutorial001.py[ln:23-34]!} @@ -261,12 +401,26 @@ Now that we have the heroes in the **session** and that we are ready to save all # More code here later 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/insert/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/insert/tutorial001.py!} ``` +//// + /// Once this line is executed, the **session** will use the **engine** to save all the data in the database by sending the corresponding SQL. @@ -294,6 +448,19 @@ if __name__ == "__main__": But to keep things a bit more organized, let's instead create a new function `main()` that will contain all the code that should be executed when called as an independent script, and we can put there the previous function `create_db_and_tables()`, and add the new function `create_heroes()`: +//// tab | Python 3.10+ + +```Python hl_lines="2 4" +# Code above omitted 👆 +{!./docs_src/tutorial/insert/tutorial002_py310.py[ln:34-36]!} + +# More code here later 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="2 4" # Code above omitted 👆 {!./docs_src/tutorial/insert/tutorial002.py[ln:36-38]!} @@ -301,27 +468,66 @@ But to keep things a bit more organized, let's instead create a new function `ma # More code here later 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/insert/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/insert/tutorial002.py!} ``` +//// + /// And then we can call that single `main()` function from that main block: +//// tab | Python 3.10+ + +```Python hl_lines="8" +# Code above omitted 👆 +{!./docs_src/tutorial/insert/tutorial002_py310.py[ln:34-40]!} +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="8" # Code above omitted 👆 {!./docs_src/tutorial/insert/tutorial002.py[ln:36-42]!} ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/insert/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/insert/tutorial002.py!} ``` +//// + /// By having everything that should happen when called as a script in a single function, we can easily add more code later on. @@ -377,6 +583,20 @@ The **session** holds some resources, like connections from the engine. So once we are done with the session, we should **close** it to make it release those resources and finish its cleanup: +//// tab | Python 3.10+ + +```Python hl_lines="16" +# Code above omitted 👆 + +{!./docs_src/tutorial/insert/tutorial001_py310.py[ln:21-34]!} + +# More code here later 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="16" # Code above omitted 👆 @@ -385,12 +605,26 @@ So once we are done with the session, we should **close** it to make it release # More code here later 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/insert/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/insert/tutorial001.py!} ``` +//// + /// But what happens if we forget to close the session? @@ -405,17 +639,42 @@ It's good to know how the `Session` works and how to create and close it manuall But there's a better way to handle the session, using a `with` block: +//// tab | Python 3.10+ + +```Python hl_lines="7-12" +# Code above omitted 👆 +{!./docs_src/tutorial/insert/tutorial002_py310.py[ln:21-31]!} +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="7-12" # Code above omitted 👆 {!./docs_src/tutorial/insert/tutorial002.py[ln:23-33]!} ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/insert/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/insert/tutorial002.py!} ``` +//// + /// This is the same as creating the session manually and then manually closing it. But here, using a `with` block, it will be automatically created when **starting** the `with` block and assigned to the variable `session`, and it will be automatically closed after the `with` block is **finished**. @@ -430,12 +689,26 @@ You already know all the first part creating the `Hero` model class, the **engin Let's focus on the new code: +//// tab | Python 3.10+ + +```{.python .annotate } +{!./docs_src/tutorial/insert/tutorial003_py310.py!} +``` + +{!./docs_src/tutorial/insert/annotations/en/tutorial003.md!} + +//// + +//// tab | Python 3.7+ + ```{.python .annotate } {!./docs_src/tutorial/insert/tutorial003.py!} ``` {!./docs_src/tutorial/insert/annotations/en/tutorial003.md!} +//// + /// tip Review what each line does by clicking each number bubble in the code. 👆 diff --git a/docs/tutorial/limit-and-offset.md b/docs/tutorial/limit-and-offset.md index b3fd1514a2..aa0d659a31 100644 --- a/docs/tutorial/limit-and-offset.md +++ b/docs/tutorial/limit-and-offset.md @@ -14,6 +14,20 @@ We will continue with the same code as before, but we'll modify it a little the Again, we will create several heroes to have some data to select from: +//// tab | Python 3.10+ + +```Python hl_lines="4-10" +# Code above omitted 👆 + +{!./docs_src/tutorial/offset_and_limit/tutorial001_py310.py[ln:21-39]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="4-10" # Code above omitted 👆 @@ -22,18 +36,46 @@ Again, we will create several heroes to have some data to select from: # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/offset_and_limit/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/offset_and_limit/tutorial001.py!} ``` +//// + /// ## Review Select All This is the code we had to select all the heroes in the `select()` examples: +//// tab | Python 3.10+ + +```Python hl_lines="3-8" +# Code above omitted 👆 + +{!./docs_src/tutorial/select/tutorial003_py310.py[ln:34-39]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3-8" # Code above omitted 👆 @@ -42,12 +84,26 @@ This is the code we had to select all the heroes in the `select()` examples: # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/select/tutorial003_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/select/tutorial003.py!} ``` +//// + /// But this would get us **all** the heroes at the same time, in a database that could have thousands, that could be problematic. @@ -56,6 +112,20 @@ But this would get us **all** the heroes at the same time, in a database that co We currently have 7 heroes in the database. But we could as well have thousands, so let's limit the results to get only the first 3: +//// tab | Python 3.10+ + +```Python hl_lines="5" +# Code above omitted 👆 + +{!./docs_src/tutorial/offset_and_limit/tutorial001_py310.py[ln:42-47]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="5" # Code above omitted 👆 @@ -64,12 +134,26 @@ We currently have 7 heroes in the database. But we could as well have thousands, # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/offset_and_limit/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/offset_and_limit/tutorial001.py!} ``` +//// + /// The special **select** object we get from `select()` also has a method `.limit()` that we can use to limit the results to a certain number. @@ -133,6 +217,20 @@ How do we get the next 3? We can use `.offset()`: +//// tab | Python 3.10+ + +```Python hl_lines="5" +# Code above omitted 👆 + +{!./docs_src/tutorial/offset_and_limit/tutorial002_py310.py[ln:42-47]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="5" # Code above omitted 👆 @@ -141,12 +239,26 @@ We can use `.offset()`: # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/offset_and_limit/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/offset_and_limit/tutorial002.py!} ``` +//// + /// The way this works is that the special **select** object we get from `select()` has methods like `.where()`, `.offset()` and `.limit()`. @@ -186,6 +298,20 @@ INFO Engine [no key 0.00020s] (3, 3) Then to get the next batch of 3 rows we would offset all the ones we already saw, the first 6: +//// tab | Python 3.10+ + +```Python hl_lines="5" +# Code above omitted 👆 + +{!./docs_src/tutorial/offset_and_limit/tutorial003_py310.py[ln:42-47]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="5" # Code above omitted 👆 @@ -194,12 +320,26 @@ Then to get the next batch of 3 rows we would offset all the ones we already saw # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/offset_and_limit/tutorial003_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/offset_and_limit/tutorial003.py!} ``` +//// + /// The database right now has **only 7 rows**, so this query can only get 1 row. @@ -255,6 +395,20 @@ If you try that in **DB Browser for SQLite**, you will get the same result: Of course, you can also combine `.limit()` and `.offset()` with `.where()` and other methods you will learn about later: +//// tab | Python 3.10+ + +```Python hl_lines="5" +# Code above omitted 👆 + +{!./docs_src/tutorial/offset_and_limit/tutorial004_py310.py[ln:42-47]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="5" # Code above omitted 👆 @@ -263,12 +417,26 @@ Of course, you can also combine `.limit()` and `.offset()` with `.where()` and o # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/offset_and_limit/tutorial004_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/offset_and_limit/tutorial004.py!} ``` +//// + /// ## Run the Program with Limit, Offset, and Where on the Command Line diff --git a/docs/tutorial/many-to-many/create-data.md b/docs/tutorial/many-to-many/create-data.md index 1925316a09..bcd8762997 100644 --- a/docs/tutorial/many-to-many/create-data.md +++ b/docs/tutorial/many-to-many/create-data.md @@ -10,16 +10,62 @@ We'll continue from where we left off with the previous code. /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/many_to_many/tutorial001.py!} ``` +//// + /// ## Create Heroes As we have done before, we'll create a function `create_heroes()` and we'll create some teams and heroes in it: +//// tab | Python 3.10+ + +```Python hl_lines="11" +# Code above omitted 👆 + +{!./docs_src/tutorial/many_to_many/tutorial001_py310.py[ln:36-54]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="11" +# Code above omitted 👆 + +{!./docs_src/tutorial/many_to_many/tutorial001_py39.py[ln:42-60]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="11" # Code above omitted 👆 @@ -28,12 +74,34 @@ As we have done before, we'll create a function `create_heroes()` and we'll crea # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/many_to_many/tutorial001.py!} ``` +//// + /// This is very similar to what we have done before. @@ -48,6 +116,32 @@ See how **Deadpond** now belongs to the two teams? Now let's do as we have done before, `commit` the **session**, `refresh` the data, and print it: +//// tab | Python 3.10+ + +```Python hl_lines="22-25 27-29 31-36" +# Code above omitted 👆 + +{!./docs_src/tutorial/many_to_many/tutorial001_py310.py[ln:36-69]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="22-25 27-29 31-36" +# Code above omitted 👆 + +{!./docs_src/tutorial/many_to_many/tutorial001_py39.py[ln:42-75]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="22-25 27-29 31-36" # Code above omitted 👆 @@ -56,19 +150,67 @@ Now let's do as we have done before, `commit` the **session**, `refresh` the dat # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/many_to_many/tutorial001.py!} ``` +//// + /// ## Add to Main As before, add the `create_heroes()` function to the `main()` function to make sure it is called when running this program from the command line: -```Python hl_lines="22-25 27-29 31-36" +//// tab | Python 3.10+ + +```Python +# Code above omitted 👆 + +{!./docs_src/tutorial/many_to_many/tutorial001_py310.py[ln:72-74]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python +# Code above omitted 👆 + +{!./docs_src/tutorial/many_to_many/tutorial001_py39.py[ln:78-80]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + +```Python # Code above omitted 👆 {!./docs_src/tutorial/many_to_many/tutorial001.py[ln:78-80]!} @@ -76,12 +218,34 @@ As before, add the `create_heroes()` function to the `main()` function to make s # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/many_to_many/tutorial001.py!} ``` +//// + /// ## Run the Program diff --git a/docs/tutorial/many-to-many/create-models-with-link.md b/docs/tutorial/many-to-many/create-models-with-link.md index 8ad06d84b6..308c678c34 100644 --- a/docs/tutorial/many-to-many/create-models-with-link.md +++ b/docs/tutorial/many-to-many/create-models-with-link.md @@ -12,18 +12,62 @@ As we want to support a **many-to-many** relationship, now we need a **link tabl We can create it just as any other **SQLModel**: +//// tab | Python 3.10+ + +```Python hl_lines="4-6" +{!./docs_src/tutorial/many_to_many/tutorial001_py310.py[ln:1-6]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="6-12" +{!./docs_src/tutorial/many_to_many/tutorial001_py39.py[ln:1-12]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="6-12" {!./docs_src/tutorial/many_to_many/tutorial001.py[ln:1-12]!} # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/many_to_many/tutorial001.py!} ``` +//// + /// This is a **SQLModel** class model table like any other. @@ -38,6 +82,32 @@ And **both fields are primary keys**. We hadn't used this before. 🤓 Let's see the `Team` model, it's almost identical as before, but with a little change: +//// tab | Python 3.10+ + +```Python hl_lines="8" +# Code above omitted 👆 + +{!./docs_src/tutorial/many_to_many/tutorial001_py310.py[ln:9-14]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="8" +# Code above omitted 👆 + +{!./docs_src/tutorial/many_to_many/tutorial001_py39.py[ln:15-20]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="8" # Code above omitted 👆 @@ -46,12 +116,34 @@ Let's see the `Team` model, it's almost identical as before, but with a little c # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/many_to_many/tutorial001.py!} ``` +//// + /// The **relationship attribute `heroes`** is still a list of heroes, annotated as `List["Hero"]`. Again, we use `"Hero"` in quotes because we haven't declared that class yet by this point in the code (but as you know, editors and **SQLModel** understand that). @@ -66,6 +158,32 @@ And here's the important part to allow the **many-to-many** relationship, we use Let's see the other side, here's the `Hero` model: +//// tab | Python 3.10+ + +```Python hl_lines="9" +# Code above omitted 👆 + +{!./docs_src/tutorial/many_to_many/tutorial001_py310.py[ln:17-23]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="9" +# Code above omitted 👆 + +{!./docs_src/tutorial/many_to_many/tutorial001_py39.py[ln:23-29]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="9" # Code above omitted 👆 @@ -74,12 +192,34 @@ Let's see the other side, here's the `Hero` model: # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/many_to_many/tutorial001.py!} ``` +//// + /// We **removed** the previous `team_id` field (column) because now the relationship is done via the link table. 🔥 @@ -98,6 +238,32 @@ And now we have a **`link_model=HeroTeamLink`**. ✨ The same as before, we will have the rest of the code to create the **engine**, and a function to create all the tables `create_db_and_tables()`. +//// tab | Python 3.10+ + +```Python hl_lines="9" +# Code above omitted 👆 + +{!./docs_src/tutorial/many_to_many/tutorial001_py310.py[ln:26-33]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="9" +# Code above omitted 👆 + +{!./docs_src/tutorial/many_to_many/tutorial001_py39.py[ln:32-39]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="9" # Code above omitted 👆 @@ -106,17 +272,67 @@ The same as before, we will have the rest of the code to create the **engine**, # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/many_to_many/tutorial001.py!} ``` +//// + /// And as in previous examples, we will add that function to a function `main()`, and we will call that `main()` function in the main block: +//// tab | Python 3.10+ + +```Python hl_lines="4" +# Code above omitted 👆 + +{!./docs_src/tutorial/many_to_many/tutorial001_py310.py[ln:72-73]!} + # We will do more stuff here later 👈 + +{!./docs_src/tutorial/many_to_many/tutorial001_py310.py[ln:77-78]!} +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="4" +# Code above omitted 👆 + +{!./docs_src/tutorial/many_to_many/tutorial001_py39.py[ln:78-79]!} + # We will do more stuff here later 👈 + +{!./docs_src/tutorial/many_to_many/tutorial001_py39.py[ln:83-84]!} +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="4" # Code above omitted 👆 @@ -126,12 +342,34 @@ And as in previous examples, we will add that function to a function `main()`, a {!./docs_src/tutorial/many_to_many/tutorial001.py[ln:83-84]!} ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/many_to_many/tutorial001.py!} ``` +//// + /// diff --git a/docs/tutorial/many-to-many/link-with-extra-fields.md b/docs/tutorial/many-to-many/link-with-extra-fields.md index b7e06c2b4e..653ef223de 100644 --- a/docs/tutorial/many-to-many/link-with-extra-fields.md +++ b/docs/tutorial/many-to-many/link-with-extra-fields.md @@ -32,6 +32,32 @@ We will add a new field `is_training`. And we will also add two **relationship attributes**, for the linked `team` and `hero`: +//// tab | Python 3.10+ + +```Python hl_lines="6 8-9" +# Code above omitted 👆 + +{!./docs_src/tutorial/many_to_many/tutorial003_py310.py[ln:4-10]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="10 12-13" +# Code above omitted 👆 + +{!./docs_src/tutorial/many_to_many/tutorial003_py39.py[ln:6-16]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="10 12-13" # Code above omitted 👆 @@ -40,12 +66,34 @@ And we will also add two **relationship attributes**, for the linked `team` and # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial003_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial003_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/many_to_many/tutorial003.py!} ``` +//// + /// The new **relationship attributes** have their own `back_populates` pointing to new relationship attributes we will create in the `Hero` and `Team` models: @@ -67,6 +115,32 @@ Now let's update the `Team` model. We no longer have the `heroes` relationship attribute, and instead we have the new `hero_links` attribute: +//// tab | Python 3.10+ + +```Python hl_lines="8" +# Code above omitted 👆 + +{!./docs_src/tutorial/many_to_many/tutorial003_py310.py[ln:13-18]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="8" +# Code above omitted 👆 + +{!./docs_src/tutorial/many_to_many/tutorial003_py39.py[ln:19-24]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="8" # Code above omitted 👆 @@ -75,12 +149,34 @@ We no longer have the `heroes` relationship attribute, and instead we have the n # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial003_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial003_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/many_to_many/tutorial003.py!} ``` +//// + /// ## Update Hero Model @@ -89,6 +185,32 @@ The same with the `Hero` model. We change the `teams` relationship attribute for `team_links`: +//// tab | Python 3.10+ + +```Python hl_lines="9" +# Code above omitted 👆 + +{!./docs_src/tutorial/many_to_many/tutorial003_py310.py[ln:21-27]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="9" +# Code above omitted 👆 + +{!./docs_src/tutorial/many_to_many/tutorial003_py39.py[ln:27-33]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="9" # Code above omitted 👆 @@ -97,12 +219,34 @@ We change the `teams` relationship attribute for `team_links`: # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial003_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial003_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/many_to_many/tutorial003.py!} ``` +//// + /// ## Create Relationships @@ -111,6 +255,32 @@ Now the process to create relationships is very similar. But now we create the **explicit link models** manually, pointing to their hero and team instances, and specifying the additional link data (`is_training`): +//// tab | Python 3.10+ + +```Python hl_lines="21-30 32-35" +# Code above omitted 👆 + +{!./docs_src/tutorial/many_to_many/tutorial003_py310.py[ln:40-79]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="21-30 32-35" +# Code above omitted 👆 + +{!./docs_src/tutorial/many_to_many/tutorial003_py39.py[ln:46-85]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="21-30 32-35" # Code above omitted 👆 @@ -119,12 +289,34 @@ But now we create the **explicit link models** manually, pointing to their hero # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial003_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial003_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/many_to_many/tutorial003.py!} ``` +//// + /// We are just adding the link model instances to the session, because the link model instances are connected to the heroes and teams, they will be also automatically included in the session when we commit. @@ -223,6 +415,32 @@ Now, to add a new relationship, we have to create a new `HeroTeamLink` instance Here we do that in the `update_heroes()` function: +//// tab | Python 3.10+ + +```Python hl_lines="10-15" +# Code above omitted 👆 + +{!./docs_src/tutorial/many_to_many/tutorial003_py310.py[ln:82-97]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="10-15" +# Code above omitted 👆 + +{!./docs_src/tutorial/many_to_many/tutorial003_py39.py[ln:88-103]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="10-15" # Code above omitted 👆 @@ -231,12 +449,34 @@ Here we do that in the `update_heroes()` function: # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial003_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial003_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/many_to_many/tutorial003.py!} ``` +//// + /// ## Run the Program with the New Relationship @@ -318,6 +558,40 @@ So now we want to update the status of `is_training` to `False`. We can do that by iterating on the links: +//// tab | Python 3.10+ + +```Python hl_lines="8-10" +# Code above omitted 👆 + +{!./docs_src/tutorial/many_to_many/tutorial003_py310.py[ln:82-83]!} + + # Code here omitted 👈 + +{!./docs_src/tutorial/many_to_many/tutorial003_py310.py[ln:99-107]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="8-10" +# Code above omitted 👆 + +{!./docs_src/tutorial/many_to_many/tutorial003_py39.py[ln:88-89]!} + + # Code here omitted 👈 + +{!./docs_src/tutorial/many_to_many/tutorial003_py39.py[ln:105-113]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="8-10" # Code above omitted 👆 @@ -330,12 +604,34 @@ We can do that by iterating on the links: # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial003_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial003_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/many_to_many/tutorial003.py!} ``` +//// + /// ## Run the Program with the Updated Relationships diff --git a/docs/tutorial/many-to-many/update-remove-relationships.md b/docs/tutorial/many-to-many/update-remove-relationships.md index 0e83e24d2d..555289a0e7 100644 --- a/docs/tutorial/many-to-many/update-remove-relationships.md +++ b/docs/tutorial/many-to-many/update-remove-relationships.md @@ -6,10 +6,30 @@ We'll continue from where we left off with the previous code. /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/many_to_many/tutorial001.py!} ``` +//// + /// ## Get Data to Update @@ -22,6 +42,36 @@ As you already know how these goes, I'll use the **short version** and get the d And because we are now using `select()`, we also have to import it. +//// tab | Python 3.10+ + +```Python hl_lines="1 5-10" +{!./docs_src/tutorial/many_to_many/tutorial002_py310.py[ln:1]!} + +# Some code here omitted 👈 + +{!./docs_src/tutorial/many_to_many/tutorial002_py310.py[ln:72-77]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="3 7-12" +{!./docs_src/tutorial/many_to_many/tutorial002_py39.py[ln:1-3]!} + +# Some code here omitted 👈 + +{!./docs_src/tutorial/many_to_many/tutorial002_py39.py[ln:78-83]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3 7-12" {!./docs_src/tutorial/many_to_many/tutorial002.py[ln:1-3]!} @@ -32,28 +82,94 @@ And because we are now using `select()`, we also have to import it. # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial002_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/many_to_many/tutorial002.py!} ``` +//// + /// And of course, we have to add `update_heroes()` to our `main()` function: +//// tab | Python 3.10+ + +```Python hl_lines="6" +# Code above omitted 👆 + +{!./docs_src/tutorial/many_to_many/tutorial002_py310.py[ln:94-101]!} +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="6" +# Code above omitted 👆 + +{!./docs_src/tutorial/many_to_many/tutorial002_py39.py[ln:100-107]!} +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="6" # Code above omitted 👆 {!./docs_src/tutorial/many_to_many/tutorial002.py[ln:100-107]!} ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial002_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/many_to_many/tutorial002.py!} ``` +//// + /// ## Add Many-to-Many Relationships @@ -62,6 +178,32 @@ Now let's imagine that **Spider-Boy** thinks that the **Z-Force** team is super We can use the same **relationship attributes** to include `hero_spider_boy` in the `team_z_force.heroes`. +//// tab | Python 3.10+ + +```Python hl_lines="10-12 14-15" +# Code above omitted 👆 + +{!./docs_src/tutorial/many_to_many/tutorial002_py310.py[ln:72-84]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="10-12 14-15" +# Code above omitted 👆 + +{!./docs_src/tutorial/many_to_many/tutorial002_py39.py[ln:78-90]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="10-12 14-15" # Code above omitted 👆 @@ -70,12 +212,34 @@ We can use the same **relationship attributes** to include `hero_spider_boy` in # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial002_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/many_to_many/tutorial002.py!} ``` +//// + /// /// tip @@ -161,6 +325,32 @@ Because `hero_spider_boy.teams` is just a list (a special list managed by SQLAlc In this case, we use the method `.remove()`, that takes an item and removes it from the list. +//// tab | Python 3.10+ + +```Python hl_lines="17-19 21-22" +# Code above omitted 👆 + +{!./docs_src/tutorial/many_to_many/tutorial002_py310.py[ln:72-91]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="17-19 21-22" +# Code above omitted 👆 + +{!./docs_src/tutorial/many_to_many/tutorial002_py39.py[ln:78-97]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="17-19 21-22" # Code above omitted 👆 @@ -169,12 +359,34 @@ In this case, we use the method `.remove()`, that takes an item and removes it f # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/many_to_many/tutorial002_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/many_to_many/tutorial002.py!} ``` +//// + /// And this time, just to show again that by using `back_populates` **SQLModel** (actually SQLAlchemy) takes care of connecting the models by their relationships, even though we performed the operation from the `hero_spider_boy` object (modifying `hero_spider_boy.teams`), we are adding `team_z_force` to the **session**. And we commit that, without even add `hero_spider_boy`. diff --git a/docs/tutorial/one.md b/docs/tutorial/one.md index 75a4cc5c7b..f374d1b4a6 100644 --- a/docs/tutorial/one.md +++ b/docs/tutorial/one.md @@ -16,10 +16,22 @@ We'll continue with the same examples we have been using in the previous chapter /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/indexes/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/indexes/tutorial002.py!} ``` +//// + /// If you already executed the previous examples and have a database with data, **remove the database file** before running each example, that way you won't have duplicate data and you will be able to get the same results. @@ -28,6 +40,20 @@ If you already executed the previous examples and have a database with data, **r We have been iterating over the rows in a `result` object like: +//// tab | Python 3.10+ + +```Python hl_lines="7-8" +# Code above omitted 👆 + +{!./docs_src/tutorial/indexes/tutorial002_py310.py[ln:42-47]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="7-8" # Code above omitted 👆 @@ -36,18 +62,46 @@ We have been iterating over the rows in a `result` object like: # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/indexes/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/indexes/tutorial002.py!} ``` +//// + /// But let's say that we are not interested in all the rows, just the **first** one. We can call the `.first()` method on the `results` object to get the first row: +//// tab | Python 3.10+ + +```Python hl_lines="7" +# Code above omitted 👆 + +{!./docs_src/tutorial/one/tutorial001_py310.py[ln:42-47]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="7" # Code above omitted 👆 @@ -56,12 +110,26 @@ We can call the `.first()` method on the `results` object to get the first row: # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/one/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/one/tutorial001.py!} ``` +//// + /// This will return the first object in the `results` (if there was any). @@ -103,6 +171,20 @@ It would be possible that the SQL query doesn't find any row. In that case, `.first()` will return `None`: +//// tab | Python 3.10+ + +```Python hl_lines="5 7" +# Code above omitted 👆 + +{!./docs_src/tutorial/one/tutorial002_py310.py[ln:42-47]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="5 7" # Code above omitted 👆 @@ -111,12 +193,26 @@ In that case, `.first()` will return `None`: # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/one/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/one/tutorial002.py!} ``` +//// + /// In this case, as there's no hero with an age less than 25, `.first()` will return `None`. @@ -150,6 +246,20 @@ And if there was more than one, it would mean that there's an error in the syste In that case, instead of `.first()` we can use `.one()`: +//// tab | Python 3.10+ + +```Python hl_lines="7" +# Code above omitted 👆 + +{!./docs_src/tutorial/one/tutorial003_py310.py[ln:42-47]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="7" # Code above omitted 👆 @@ -158,12 +268,26 @@ In that case, instead of `.first()` we can use `.one()`: # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/one/tutorial003_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/one/tutorial003.py!} ``` +//// + /// Here we know that there's only one `"Deadpond"`, and there shouldn't be any more than one. @@ -220,6 +344,20 @@ sqlalchemy.exc.MultipleResultsFound: Multiple rows were found when exactly one w Of course, even if we don't duplicate the data, we could get the same error if we send a query that finds more than one row and expect exactly one with `.one()`: +//// tab | Python 3.10+ + +```Python hl_lines="5 7" +# Code above omitted 👆 + +{!./docs_src/tutorial/one/tutorial004_py310.py[ln:42-47]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="5 7" # Code above omitted 👆 @@ -228,12 +366,26 @@ Of course, even if we don't duplicate the data, we could get the same error if w # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/one/tutorial004_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/one/tutorial004.py!} ``` +//// + /// That would find 2 rows, and would end up with the same error. @@ -242,6 +394,20 @@ That would find 2 rows, and would end up with the same error. And also, if we get no rows at all with `.one()`, it will also raise an error: +//// tab | Python 3.10+ + +```Python hl_lines="5 7" +# Code above omitted 👆 + +{!./docs_src/tutorial/one/tutorial005_py310.py[ln:42-47]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="5 7" # Code above omitted 👆 @@ -250,12 +416,26 @@ And also, if we get no rows at all with `.one()`, it will also raise an error: # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/one/tutorial005_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/one/tutorial005.py!} ``` +//// + /// In this case, as there are no heroes with an age less than 25, `.one()` will raise an error. @@ -289,6 +469,20 @@ sqlalchemy.exc.NoResultFound: No row was found when one was required Of course, with `.first()` and `.one()` you would also probably write all that in a more compact form most of the time, all in a single line (or at least a single Python statement): +//// tab | Python 3.10+ + +```Python hl_lines="5" +# Code above omitted 👆 + +{!./docs_src/tutorial/one/tutorial006_py310.py[ln:42-45]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="5" # Code above omitted 👆 @@ -297,12 +491,26 @@ Of course, with `.first()` and `.one()` you would also probably write all that i # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/one/tutorial006_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/one/tutorial006.py!} ``` +//// + /// That would result in the same as some examples above. @@ -313,6 +521,20 @@ In many cases you might want to select a single row by its Id column with the ** You could do it the same way we have been doing with a `.where()` and then getting the first item with `.first()`: +//// tab | Python 3.10+ + +```Python hl_lines="5 7" +# Code above omitted 👆 + +{!./docs_src/tutorial/one/tutorial007_py310.py[ln:42-47]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="5 7" # Code above omitted 👆 @@ -321,12 +543,26 @@ You could do it the same way we have been doing with a `.where()` and then getti # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/one/tutorial007_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/one/tutorial007.py!} ``` +//// + /// That would work correctly, as expected. But there's a shorter version. 👇 @@ -335,6 +571,20 @@ That would work correctly, as expected. But there's a shorter version. 👇 As selecting a single row by its Id column with the **primary key** is a common operation, there's a shortcut for it: +//// tab | Python 3.10+ + +```Python hl_lines="5" +# Code above omitted 👆 + +{!./docs_src/tutorial/one/tutorial008_py310.py[ln:42-45]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="5" # Code above omitted 👆 @@ -343,12 +593,26 @@ As selecting a single row by its Id column with the **primary key** is a common # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/one/tutorial008_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/one/tutorial008.py!} ``` +//// + /// `session.get(Hero, 1)` is an equivalent to creating a `select()`, then filtering by Id using `.where()`, and then getting the first item with `.first()`. @@ -378,6 +642,20 @@ Hero: secret_name='Dive Wilson' age=None id=1 name='Deadpond' `.get()` behaves similar to `.first()`, if there's no data it will simply return `None` (instead of raising an error): +//// tab | Python 3.10+ + +```Python hl_lines="5" +# Code above omitted 👆 + +{!./docs_src/tutorial/one/tutorial009_py310.py[ln:42-45]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="5" # Code above omitted 👆 @@ -386,12 +664,26 @@ Hero: secret_name='Dive Wilson' age=None id=1 name='Deadpond' # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/one/tutorial009_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/one/tutorial009.py!} ``` +//// + /// Running that will output: diff --git a/docs/tutorial/relationship-attributes/back-populates.md b/docs/tutorial/relationship-attributes/back-populates.md index 0c48bd790a..0eaa7bb650 100644 --- a/docs/tutorial/relationship-attributes/back-populates.md +++ b/docs/tutorial/relationship-attributes/back-populates.md @@ -20,18 +20,62 @@ Let's understand that better with an example. Let's see how that works by writing an **incomplete** version first, without `back_populates`: +//// tab | Python 3.10+ + +```Python hl_lines="9 19" +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py[ln:1-19]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="11 21" +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py[ln:1-21]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="11 21" {!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py[ln:1-21]!} # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py!} ``` +//// + /// ## Read Data Objects @@ -40,6 +84,32 @@ Now, we will get the **Spider-Boy** hero and, *independently*, the **Preventers* As you already know how this works, I won't separate that in a select `statement`, `results`, etc. Let's use the shorter form in a single call: +//// tab | Python 3.10+ + +```Python hl_lines="5-7 9-11" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py[ln:103-111]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="5-7 9-11" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py[ln:105-113]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="5-7 9-11" # Code above omitted 👆 @@ -48,12 +118,34 @@ As you already know how this works, I won't separate that in a select `statement # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py!} ``` +//// + /// /// tip @@ -66,6 +158,32 @@ When writing your own code, this is probably the style you will use most often, Now, let's print the current **Spider-Boy**, the current **Preventers** team, and particularly, the current **Preventers** list of heroes: +//// tab | Python 3.10+ + +```Python hl_lines="13-15" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py[ln:103-115]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="13-15" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py[ln:105-117]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="13-15" # Code above omitted 👆 @@ -74,12 +192,34 @@ Now, let's print the current **Spider-Boy**, the current **Preventers** team, an # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py!} ``` +//// + /// Up to this point, it's all good. 😊 @@ -102,6 +242,40 @@ Notice that we have **Spider-Boy** there. Now let's update **Spider-Boy**, removing him from the team by setting `hero_spider_boy.team = None` and then let's print this object again: +//// tab | Python 3.10+ + +```Python hl_lines="8 12" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py[ln:103-104]!} + + # Code here omitted 👈 + +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py[ln:117-121]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="8 12" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py[ln:105-106]!} + + # Code here omitted 👈 + +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py[ln:119-123]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="8 12" # Code above omitted 👆 @@ -114,12 +288,34 @@ Now let's update **Spider-Boy**, removing him from the team by setting `hero_spi # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py!} ``` +//// + /// The first important thing is, we *haven't committed* the hero yet, so accessing the list of heroes would not trigger an automatic refresh. @@ -160,6 +356,40 @@ Oh, no! 😱 **Spider-Boy** is still listed there! Now, if we commit it and print again: +//// tab | Python 3.10+ + +```Python hl_lines="8-9 15" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py[ln:103-104]!} + + # Code here omitted 👈 + +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py[ln:123-130]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="8-9 15" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py[ln:105-106]!} + + # Code here omitted 👈 + +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py[ln:125-132]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="8-9 15" # Code above omitted 👆 @@ -172,12 +402,34 @@ Now, if we commit it and print again: # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py!} ``` +//// + /// When we access `preventers_team.heroes` after the `commit`, that triggers a refresh, so we get the latest list, without **Spider-Boy**, so that's fine again: @@ -210,22 +462,100 @@ That's what `back_populates` is for. ✨ Let's add it back: +//// tab | Python 3.10+ + +```Python hl_lines="9 19" +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py[ln:1-19]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="11 21" +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py39.py[ln:1-21]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="11 21" {!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py[ln:1-21]!} # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py!} ``` +//// + /// And we can keep the rest of the code the same: +//// tab | Python 3.10+ + +```Python hl_lines="8 12" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py[ln:103-104]!} + + # Code here omitted 👈 + +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py[ln:117-121]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="8 12" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py39.py[ln:105-106]!} + + # Code here omitted 👈 + +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py39.py[ln:119-123]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="8 12" # Code above omitted 👆 @@ -238,12 +568,34 @@ And we can keep the rest of the code the same: # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py!} ``` +//// + /// /// tip @@ -277,18 +629,62 @@ Now that you know why `back_populates` is there, let's review the exact value ag It's quite simple code, it's just a string, but it might be confusing to think exactly *what* string should go there: +//// tab | Python 3.10+ + +```Python hl_lines="9 19" +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py[ln:1-19]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="11 21" +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py39.py[ln:1-21]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="11 21" {!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py[ln:1-21]!} # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py!} ``` +//// + /// The string in `back_populates` is the name of the attribute *in the other* model, that will reference *the current* model. @@ -297,6 +693,32 @@ The string in `back_populates` is the name of the attribute *in the other* model So, in the class `Team`, we have an attribute `heroes` and we declare it with `Relationship(back_populates="team")`. +//// tab | Python 3.10+ + +```Python hl_lines="8" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py[ln:4-9]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="8" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py39.py[ln:6-11]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="8" # Code above omitted 👆 @@ -305,12 +727,34 @@ So, in the class `Team`, we have an attribute `heroes` and we declare it with `R # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py!} ``` +//// + /// The string in `back_populates="team"` refers to the attribute `team` in the class `Hero` (the other class). @@ -319,6 +763,32 @@ And, in the class `Hero`, we declare an attribute `team`, and we declare it with So, the string `"heroes"` refers to the attribute `heroes` in the class `Team`. +//// tab | Python 3.10+ + +```Python hl_lines="10" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py[ln:12-19]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="10" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py39.py[ln:14-21]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="10" # Code above omitted 👆 @@ -327,12 +797,34 @@ So, the string `"heroes"` refers to the attribute `heroes` in the class `Team`. # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py!} ``` +//// + /// /// tip @@ -358,6 +850,32 @@ So, `back_populates` would most probably be something like `"hero"` or `"heroes" +//// tab | Python 3.10+ + +```Python hl_lines="3 10 13 15" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial003_py310.py[ln:27-39]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="3 10 13 15" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial003_py39.py[ln:29-41]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3 10 13 15" # Code above omitted 👆 @@ -366,10 +884,32 @@ So, `back_populates` would most probably be something like `"hero"` or `"heroes" # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial003_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial003_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py!} ``` +//// + /// diff --git a/docs/tutorial/relationship-attributes/create-and-update-relationships.md b/docs/tutorial/relationship-attributes/create-and-update-relationships.md index e939141193..8b34f0cb53 100644 --- a/docs/tutorial/relationship-attributes/create-and-update-relationships.md +++ b/docs/tutorial/relationship-attributes/create-and-update-relationships.md @@ -6,6 +6,20 @@ Let's see now how to create data with relationships using these new **relationsh Let's check the old code we used to create some heroes and teams: +//// tab | Python 3.10+ + +```Python hl_lines="9 12 18 24" +# Code above omitted 👆 + +{!./docs_src/tutorial/connect/insert/tutorial001_py310.py[ln:29-58]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="9 12 18 24" # Code above omitted 👆 @@ -14,12 +28,26 @@ Let's check the old code we used to create some heroes and teams: # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/connect/insert/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/connect/insert/tutorial001.py!} ``` +//// + /// There are several things to **notice** here. @@ -40,6 +68,32 @@ This is the first area where these **relationship attributes** can help. 🤓 Now let's do all that, but this time using the new, shiny `Relationship` attributes: +//// tab | Python 3.10+ + +```Python hl_lines="9 12 18" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py[ln:32-55]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="9 12 18" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py39.py[ln:34-57]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="9 12 18" # Code above omitted 👆 @@ -48,12 +102,34 @@ Now let's do all that, but this time using the new, shiny `Relationship` attribu # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py!} ``` +//// + /// Now we can create the `Team` instances and pass them directly to the new `team` argument when creating the `Hero` instances, as `team=team_preventers` instead of `team_id=team_preventers.id`. @@ -70,6 +146,40 @@ And then, as you can see, we only have to do one `commit()`. The same way we could assign an integer with a `team.id` to a `hero.team_id`, we can also assign the `Team` instance to the `hero.team`: +//// tab | Python 3.10+ + +```Python hl_lines="8" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py[ln:32-33]!} + + # Previous code here omitted 👈 + +{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py[ln:57-61]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="8" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py39.py[ln:34-35]!} + + # Previous code here omitted 👈 + +{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py39.py[ln:59-63]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="8" # Code above omitted 👆 @@ -82,12 +192,34 @@ The same way we could assign an integer with a `team.id` to a `hero.team_id`, we # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py!} ``` +//// + /// ## Create a Team with Heroes @@ -96,6 +228,40 @@ Before, we created some `Team` instances and passed them in the `team=` argument We could also create the `Hero` instances first, and then pass them in the `heroes=` argument that takes a list, when creating a `Team` instance: +//// tab | Python 3.10+ + +```Python hl_lines="13 15-16" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py[ln:32-33]!} + + # Previous code here omitted 👈 + +{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py[ln:63-73]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="13 15-16" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py39.py[ln:34-35]!} + + # Previous code here omitted 👈 + +{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py39.py[ln:65-75]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="13 15-16" # Code above omitted 👆 @@ -108,12 +274,34 @@ We could also create the `Hero` instances first, and then pass them in the `hero # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py!} ``` +//// + /// Here we create two heroes first, **Black Lion** and **Princess Sure-E**, and then we pass them in the `heroes` argument. @@ -130,6 +318,40 @@ As the attribute `team.heroes` behaves like a list, we can simply append to it. Let's create some more heroes and add them to the `team_preventers.heroes` list attribute: +//// tab | Python 3.10+ + +```Python hl_lines="14-18" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py[ln:32-33]!} + + # Previous code here omitted 👈 + +{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py[ln:75-91]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="14-18" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py39.py[ln:34-35]!} + + # Previous code here omitted 👈 + +{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py39.py[ln:77-93]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="14-18" # Code above omitted 👆 @@ -142,12 +364,34 @@ Let's create some more heroes and add them to the `team_preventers.heroes` list # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py!} ``` +//// + /// The attribute `team_preventers.heroes` behaves like a list. But it's a special type of list, because when we modify it adding heroes to it, **SQLModel** (actually SQLAlchemy) **keeps track of the necessary changes** to be done in the database. diff --git a/docs/tutorial/relationship-attributes/define-relationships-attributes.md b/docs/tutorial/relationship-attributes/define-relationships-attributes.md index a5363ab027..adbcc7df75 100644 --- a/docs/tutorial/relationship-attributes/define-relationships-attributes.md +++ b/docs/tutorial/relationship-attributes/define-relationships-attributes.md @@ -41,18 +41,44 @@ Now that you know how these tables work underneath and how the model classes rep Up to now, we have only used the `team_id` column to connect the tables when querying with `select()`: +//// tab | Python 3.10+ + +```Python hl_lines="16" +{!./docs_src/tutorial/connect/insert/tutorial001_py310.py[ln:1-16]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="18" {!./docs_src/tutorial/connect/insert/tutorial001.py[ln:1-18]!} # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/connect/insert/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/connect/insert/tutorial001.py!} ``` +//// + /// This is a **plain field** like all the others, all representing a **column in the table**. @@ -61,34 +87,122 @@ But now let's add a couple of new special attributes to these model classes, let First, import `Relationship` from `sqlmodel`: +//// tab | Python 3.10+ + +```Python hl_lines="1" +{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py[ln:1]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="3" +{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py39.py[ln:1-3]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3" {!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py[ln:1-3]!} # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py!} ``` +//// + /// Next, use that `Relationship` to declare a new attribute in the model classes: +//// tab | Python 3.10+ + +```Python hl_lines="9 19" +{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py[ln:1-19]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="11 21" +{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py39.py[ln:1-21]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="11 21" {!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py[ln:1-21]!} # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py!} ``` +//// + /// ## What Are These Relationship Attributes diff --git a/docs/tutorial/relationship-attributes/read-relationships.md b/docs/tutorial/relationship-attributes/read-relationships.md index e4a049d2bd..fe9e5ae02a 100644 --- a/docs/tutorial/relationship-attributes/read-relationships.md +++ b/docs/tutorial/relationship-attributes/read-relationships.md @@ -6,6 +6,40 @@ Now that we know how to connect data using **relationship Attributes**, let's se First, add a function `select_heroes()` where we get a hero to start working with, and add that function to the `main()` function: +//// tab | Python 3.10+ + +```Python hl_lines="3-7 14" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py310.py[ln:94-98]!} + +# Previous code here omitted 👈 + +{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py310.py[ln:108-111]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="3-7 14" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py39.py[ln:96-100]!} + +# Previous code here omitted 👈 + +{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py39.py[ln:110-113]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3-7 14" # Code above omitted 👆 @@ -18,12 +52,34 @@ First, add a function `select_heroes()` where we get a hero to start working wit # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py!} ``` +//// + /// ## Select the Related Team - Old Way @@ -32,6 +88,32 @@ Now that we have a hero, we can get the team this hero belongs to. With what we have learned **up to now**, we could use a `select()` statement, then execute it with `session.exec()`, and then get the `.first()` result, for example: +//// tab | Python 3.10+ + +```Python hl_lines="9-12" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py310.py[ln:94-103]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="9-12" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py39.py[ln:96-105]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="9-12" # Code above omitted 👆 @@ -40,12 +122,34 @@ With what we have learned **up to now**, we could use a `select()` statement, th # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py!} ``` +//// + /// ## Get Relationship Team - New Way @@ -54,6 +158,40 @@ But now that we have the **relationship attributes**, we can just access them, a So, the highlighted block above, has the same results as the block below: +//// tab | Python 3.10+ + +```Python hl_lines="11" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py310.py[ln:94-98]!} + + # Code from the previous example omitted 👈 + +{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py310.py[ln:105]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="11" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py39.py[ln:96-100]!} + + # Code from the previous example omitted 👈 + +{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py39.py[ln:107]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="11" # Code above omitted 👆 @@ -66,12 +204,34 @@ So, the highlighted block above, has the same results as the block below: # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py!} ``` +//// + /// /// tip @@ -86,6 +246,32 @@ For example, here, **inside** a `with` block with a `Session` object. And the same way, when we are working on the **many** side of the **one-to-many** relationship, we can get a list of of the related objects just by accessing the relationship attribute: +//// tab | Python 3.10+ + +```Python hl_lines="9" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py310.py[ln:94-100]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="9" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py39.py[ln:96-102]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="9" # Code above omitted 👆 @@ -94,12 +280,34 @@ And the same way, when we are working on the **many** side of the **one-to-many* # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py!} ``` +//// + /// That would print a list with all the heroes in the Preventers team: diff --git a/docs/tutorial/relationship-attributes/remove-relationships.md b/docs/tutorial/relationship-attributes/remove-relationships.md index 982e316c2b..56745850b5 100644 --- a/docs/tutorial/relationship-attributes/remove-relationships.md +++ b/docs/tutorial/relationship-attributes/remove-relationships.md @@ -8,6 +8,32 @@ And then for some reason needs to leave the **Preventers** for some years. 😭 We can remove the relationship by setting it to `None`, the same as with the `team_id`, it also works with the new relationship attribute `.team`: +//// tab | Python 3.10+ + +```Python hl_lines="9" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py310.py[ln:103-114]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="9" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py39.py[ln:105-116]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="9" # Code above omitted 👆 @@ -16,16 +42,64 @@ We can remove the relationship by setting it to `None`, the same as with the `te # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py!} ``` +//// + /// And of course, we should remember to add this `update_heroes()` function to `main()` so that it runs when we call this program from the command line: +//// tab | Python 3.10+ + +```Python hl_lines="7" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py310.py[ln:117-121]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="7" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py39.py[ln:119-123]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="7" # Code above omitted 👆 @@ -34,12 +108,34 @@ And of course, we should remember to add this `update_heroes()` function to `mai # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py!} ``` +//// + /// ## Recap diff --git a/docs/tutorial/relationship-attributes/type-annotation-strings.md b/docs/tutorial/relationship-attributes/type-annotation-strings.md index 780da6a962..a798af9aac 100644 --- a/docs/tutorial/relationship-attributes/type-annotation-strings.md +++ b/docs/tutorial/relationship-attributes/type-annotation-strings.md @@ -2,18 +2,62 @@ In the first Relationship attribute, we declare it with `List["Hero"]`, putting the `Hero` in quotes instead of just normally there: +//// tab | Python 3.10+ + +```Python hl_lines="9" +{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py[ln:1-19]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="11" +{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py39.py[ln:1-21]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="11" {!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py[ln:1-21]!} # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py!} ``` +//// + /// What's that about? Can't we just write it normally as `List[Hero]`? diff --git a/docs/tutorial/select.md b/docs/tutorial/select.md index 5be5e8a0ba..47c80a2661 100644 --- a/docs/tutorial/select.md +++ b/docs/tutorial/select.md @@ -25,10 +25,22 @@ Let's continue from the last code we used to create some data. /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/insert/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/insert/tutorial002.py!} ``` +//// + /// We are creating a **SQLModel** `Hero` class model and creating some records. @@ -166,6 +178,20 @@ The first step is to create a **Session**, the same way we did when creating the We will start with that in a new function `select_heroes()`: +//// tab | Python 3.10+ + +```Python hl_lines="3-4" +# Code above omitted 👆 + +{!./docs_src/tutorial/select/tutorial001_py310.py[ln:34-35]!} + +# More code here later 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3-4" # Code above omitted 👆 @@ -174,12 +200,26 @@ We will start with that in a new function `select_heroes()`: # More code here later 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/select/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/select/tutorial001.py!} ``` +//// + /// ## Create a `select` Statement @@ -188,22 +228,64 @@ Next, pretty much the same way we wrote a SQL `SELECT` statement above, now we'l First we have to import `select` from `sqlmodel` at the top of the file: +//// tab | Python 3.10+ + +```Python hl_lines="1" +{!./docs_src/tutorial/select/tutorial001_py310.py[ln:1]!} + +# More code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3" {!./docs_src/tutorial/select/tutorial001.py[ln:1-3]!} # More code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/select/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/select/tutorial001.py!} ``` +//// + /// And then we will use it to create a `SELECT` statement in Python code: +//// tab | Python 3.10+ + +```Python hl_lines="7" +{!./docs_src/tutorial/select/tutorial001_py310.py[ln:1]!} + +# More code here omitted 👈 + +{!./docs_src/tutorial/select/tutorial001_py310.py[ln:34-36]!} + +# More code here later 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="9" {!./docs_src/tutorial/select/tutorial001.py[ln:1-3]!} @@ -214,12 +296,26 @@ And then we will use it to create a `SELECT` statement in Python code: # More code here later 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/select/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/select/tutorial001.py!} ``` +//// + /// It's a very simple line of code that conveys a lot of information: @@ -251,6 +347,20 @@ I'll tell you about that in the next chapters. Now that we have the `select` statement, we can execute it with the **session**: +//// tab | Python 3.10+ + +```Python hl_lines="6" +# Code above omitted 👆 + +{!./docs_src/tutorial/select/tutorial001_py310.py[ln:34-37]!} + +# More code here later 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="6" # Code above omitted 👆 @@ -259,12 +369,26 @@ Now that we have the `select` statement, we can execute it with the **session**: # More code here later 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/select/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/select/tutorial001.py!} ``` +//// + /// This will tell the **session** to go ahead and use the **engine** to execute that `SELECT` statement in the database and bring the results back. @@ -303,6 +427,20 @@ The `results` object is an ` to get the rows where a column is **more than** a value: +//// tab | Python 3.10+ + +```Python hl_lines="5" +# Code above omitted 👆 + +{!./docs_src/tutorial/where/tutorial003_py310.py[ln:42-47]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="5" # Code above omitted 👆 @@ -589,12 +755,26 @@ Now let's use `>` to get the rows where a column is **more than** a value: # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/where/tutorial003_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/where/tutorial003.py!} ``` +//// + /// That would output: @@ -615,6 +795,20 @@ Notice that it didn't select `Black Lion`, because the age is not *strictly* gre Let's do that again, but with `>=` to get the rows where a column is **more than or equal** to a value: +//// tab | Python 3.10+ + +```Python hl_lines="5" +# Code above omitted 👆 + +{!./docs_src/tutorial/where/tutorial004_py310.py[ln:42-47]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="5" # Code above omitted 👆 @@ -623,12 +817,26 @@ Let's do that again, but with `>=` to get the rows where a column is **more than # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/where/tutorial004_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/where/tutorial004.py!} ``` +//// + /// Because we are using `>=`, the age `35` will be included in the output: @@ -650,6 +858,20 @@ This time we got `Black Lion` too because although the age is not *strictly* gre Similarly, we can use `<` to get the rows where a column is **less than** a value: +//// tab | Python 3.10+ + +```Python hl_lines="5" +# Code above omitted 👆 + +{!./docs_src/tutorial/where/tutorial005_py310.py[ln:42-47]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="5" # Code above omitted 👆 @@ -658,12 +880,26 @@ Similarly, we can use `<` to get the rows where a column is **less than** a valu # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/where/tutorial005_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/where/tutorial005.py!} ``` +//// + /// And we get the younger one with an age in the database: @@ -682,6 +918,20 @@ We could imagine that **Spider-Boy** is even **younger**. But because we don't k Finally, we can use `<=` to get the rows where a column is **less than or equal** to a value: +//// tab | Python 3.10+ + +```Python hl_lines="5" +# Code above omitted 👆 + +{!./docs_src/tutorial/where/tutorial006_py310.py[ln:42-47]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="5" # Code above omitted 👆 @@ -690,12 +940,26 @@ Finally, we can use `<=` to get the rows where a column is **less than or equal* # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/where/tutorial006_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/where/tutorial006.py!} ``` +//// + /// And we get the younger ones, `35` and below: @@ -721,6 +985,20 @@ We can use the same standard Python comparison operators like `<`, `<=`, `>`, `> Because `.where()` returns the same special select object back, we can add more `.where()` calls to it: +//// tab | Python 3.10+ + +```Python hl_lines="5" +# Code above omitted 👆 + +{!./docs_src/tutorial/where/tutorial007_py310.py[ln:42-47]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="5" # Code above omitted 👆 @@ -729,12 +1007,26 @@ Because `.where()` returns the same special select object back, we can add more # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/where/tutorial007_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/where/tutorial007.py!} ``` +//// + /// This will select the rows `WHERE` the `age` is **greater than or equal** to `35`, `AND` also the `age` is **less than** `40`. @@ -776,6 +1068,20 @@ age=36 id=6 name='Dr. Weird' secret_name='Steve Weird' As an alternative to using multiple `.where()` we can also pass several expressions to a single `.where()`: +//// tab | Python 3.10+ + +```Python hl_lines="5" +# Code above omitted 👆 + +{!./docs_src/tutorial/where/tutorial008_py310.py[ln:42-47]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="5" # Code above omitted 👆 @@ -784,12 +1090,26 @@ As an alternative to using multiple `.where()` we can also pass several expressi # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/where/tutorial008_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/where/tutorial008.py!} ``` +//// + /// This is the same as the above, and will result in the same output with the two heroes: @@ -807,24 +1127,64 @@ But we can also combine expressions using `OR`. Which means that **any** (but no To do it, you can import `or_`: +//// tab | Python 3.10+ + +```Python hl_lines="1" +{!./docs_src/tutorial/where/tutorial009_py310.py[ln:1]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3" {!./docs_src/tutorial/where/tutorial009.py[ln:1-3]!} # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/where/tutorial009_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/where/tutorial009.py!} ``` +//// + /// And then pass both expressions to `or_()` and put it inside `.where()`. For example, here we select the heroes that are the youngest OR the oldest: +//// tab | Python 3.10+ + +```Python hl_lines="5" +# Code above omitted 👆 + +{!./docs_src/tutorial/where/tutorial009_py310.py[ln:42-47]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="5" # Code above omitted 👆 @@ -833,12 +1193,26 @@ For example, here we select the heroes that are the youngest OR the oldest: # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/where/tutorial009_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/where/tutorial009.py!} ``` +//// + /// When we run it, this generates the output: @@ -890,22 +1264,62 @@ We can tell the editor that this class attribute is actually a special **SQLMode To do that, we can import `col()` (as short for "column"): +//// tab | Python 3.10+ + +```Python hl_lines="1" +{!./docs_src/tutorial/where/tutorial011_py310.py[ln:1]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="3" {!./docs_src/tutorial/where/tutorial011.py[ln:1-3]!} # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/where/tutorial011_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/where/tutorial011.py!} ``` +//// + /// And then put the **class attribute** inside `col()` when using it in a `.where()`: +//// tab | Python 3.10+ + +```Python hl_lines="5" +# Code above omitted 👆 + +{!./docs_src/tutorial/where/tutorial011_py310.py[ln:42-47]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + ```Python hl_lines="5" # Code above omitted 👆 @@ -914,12 +1328,26 @@ And then put the **class attribute** inside `col()` when using it in a `.where() # Code below omitted 👇 ``` +//// + /// details | 👀 Full file preview +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/where/tutorial011_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + ```Python {!./docs_src/tutorial/where/tutorial011.py!} ``` +//// + /// So, now the comparison is not: diff --git a/docs_src/tutorial/fastapi/update/tutorial002_py310.py b/docs_src/tutorial/fastapi/update/tutorial002_py310.py index 84efb3d2a9..6be274ef75 100644 --- a/docs_src/tutorial/fastapi/update/tutorial002_py310.py +++ b/docs_src/tutorial/fastapi/update/tutorial002_py310.py @@ -87,12 +87,12 @@ def update_hero(hero_id: int, hero: HeroUpdate): if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") hero_data = hero.model_dump(exclude_unset=True) - update_data = {} + extra_data = {} if "password" in hero_data: password = hero_data["password"] hashed_password = hash_password(password) - update_data["hashed_password"] = hashed_password - db_hero.sqlmodel_update(hero_data, update=update_data) + extra_data["hashed_password"] = hashed_password + db_hero.sqlmodel_update(hero_data, update=extra_data) session.add(db_hero) session.commit() session.refresh(db_hero) diff --git a/docs_src/tutorial/fastapi/update/tutorial002_py39.py b/docs_src/tutorial/fastapi/update/tutorial002_py39.py index 72751dac3d..19d30ea6fb 100644 --- a/docs_src/tutorial/fastapi/update/tutorial002_py39.py +++ b/docs_src/tutorial/fastapi/update/tutorial002_py39.py @@ -89,12 +89,12 @@ def update_hero(hero_id: int, hero: HeroUpdate): if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") hero_data = hero.model_dump(exclude_unset=True) - update_data = {} + extra_data = {} if "password" in hero_data: password = hero_data["password"] hashed_password = hash_password(password) - update_data["hashed_password"] = hashed_password - db_hero.sqlmodel_update(hero_data, update=update_data) + extra_data["hashed_password"] = hashed_password + db_hero.sqlmodel_update(hero_data, update=extra_data) session.add(db_hero) session.commit() session.refresh(db_hero) From 0e3154a553dac424e69bae83445e7d30e1e1b039 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 21 Mar 2024 22:49:58 +0000 Subject: [PATCH 341/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 7609024f16..5574acc58e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Docs + +* ✨ Add source examples for Python 3.10 and 3.9 with updated syntax. PR [#842](https://github.com/tiangolo/sqlmodel/pull/842) by [@tiangolo](https://github.com/tiangolo). + ### Internal * 🔥 Remove Jina QA Bot as it has been discontinued. PR [#840](https://github.com/tiangolo/sqlmodel/pull/840) by [@tiangolo](https://github.com/tiangolo). From 9d0b8b6a93b31a649e6b8751e8824085c7cd0436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 21 Mar 2024 17:54:34 -0500 Subject: [PATCH 342/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 5574acc58e..a1a854eecc 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,7 +4,7 @@ ### Docs -* ✨ Add source examples for Python 3.10 and 3.9 with updated syntax. PR [#842](https://github.com/tiangolo/sqlmodel/pull/842) by [@tiangolo](https://github.com/tiangolo). +* ✨ Add source examples for Python 3.10 and 3.9 with updated syntax. PR [#842](https://github.com/tiangolo/sqlmodel/pull/842) by [@tiangolo](https://github.com/tiangolo) and [@estebanx64](https://github.com/estebanx64). ### Internal From bf51a11dcf1d9ff1bf039827e55ea33fe76408a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 26 Mar 2024 12:35:15 -0500 Subject: [PATCH 343/906] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20Ruff=20v?= =?UTF-8?q?ersion=20and=20configs=20(#859)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 2 +- pyproject.toml | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b21e5ac9cc..3289dd0950 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.6 + rev: v0.2.0 hooks: - id: ruff args: diff --git a/pyproject.toml b/pyproject.toml index 10d73793d2..9da631b985 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ cairosvg = "^2.5.2" mdx-include = "^1.4.1" coverage = {extras = ["toml"], version = ">=6.2,<8.0"} fastapi = "^0.103.2" -ruff = "^0.1.2" +ruff = "0.2.0" # For FastAPI tests httpx = "0.24.1" # TODO: upgrade when deprecating Python 3.7 @@ -92,14 +92,14 @@ disallow_incomplete_defs = false disallow_untyped_defs = false disallow_untyped_calls = false -[tool.ruff] +[tool.ruff.lint] select = [ "E", # pycodestyle errors "W", # pycodestyle warnings "F", # pyflakes "I", # isort - "C", # flake8-comprehensions "B", # flake8-bugbear + "C4", # flake8-comprehensions "UP", # pyupgrade ] ignore = [ @@ -109,12 +109,12 @@ ignore = [ "W191", # indentation contains tabs ] -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] # "__init__.py" = ["F401"] -[tool.ruff.isort] +[tool.ruff.lint.isort] known-third-party = ["sqlmodel", "sqlalchemy", "pydantic", "fastapi"] -[tool.ruff.pyupgrade] +[tool.ruff.lint.pyupgrade] # Preserve types, even if a file imports `from __future__ import annotations`. keep-runtime-typing = true From 51df778420a4a032f0b093e91470402e45b6595c Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 26 Mar 2024 17:35:41 +0000 Subject: [PATCH 344/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index a1a854eecc..512e7f1e28 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* ⬆️ Upgrade Ruff version and configs. PR [#859](https://github.com/tiangolo/sqlmodel/pull/859) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove Jina QA Bot as it has been discontinued. PR [#840](https://github.com/tiangolo/sqlmodel/pull/840) by [@tiangolo](https://github.com/tiangolo). ## 0.0.16 From 355f48860c9f9261a916b81a716130d08531f713 Mon Sep 17 00:00:00 2001 From: Esteban Maya Date: Mon, 1 Apr 2024 20:50:48 -0500 Subject: [PATCH 345/906] =?UTF-8?q?=F0=9F=91=B7=20Add=20cron=20to=20run=20?= =?UTF-8?q?test=20once=20a=20week=20on=20monday=20(#869)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 89da640d15..990bf46de4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,6 +14,9 @@ on: description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' required: false default: 'false' + schedule: + # cron every week on monday + - cron: "0 0 * * 1" jobs: test: From c75743d9d358e314bad89875c435aac40fe54334 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 2 Apr 2024 01:51:04 +0000 Subject: [PATCH 346/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 512e7f1e28..463851ecb6 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* 👷 Add cron to run test once a week on monday. PR [#869](https://github.com/tiangolo/sqlmodel/pull/869) by [@estebanx64](https://github.com/estebanx64). * ⬆️ Upgrade Ruff version and configs. PR [#859](https://github.com/tiangolo/sqlmodel/pull/859) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove Jina QA Bot as it has been discontinued. PR [#840](https://github.com/tiangolo/sqlmodel/pull/840) by [@tiangolo](https://github.com/tiangolo). From fce7ee21a25a65988de3db81672133331b03cc2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 9 Apr 2024 02:52:21 +0400 Subject: [PATCH 347/906] =?UTF-8?q?=F0=9F=94=A7=20Update=20MkDocs,=20disab?= =?UTF-8?q?le=20cards=20while=20I=20can=20upgrade=20to=20the=20latest=20Mk?= =?UTF-8?q?Docs=20Material,=20that=20fixes=20an=20issue=20with=20social=20?= =?UTF-8?q?cards=20(#888)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mkdocs.insiders.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mkdocs.insiders.yml b/mkdocs.insiders.yml index d24d754930..f59d3f3327 100644 --- a/mkdocs.insiders.yml +++ b/mkdocs.insiders.yml @@ -1,3 +1,4 @@ plugins: - social: + # TODO: Re-enable once this is fixed: https://github.com/squidfunk/mkdocs-material/issues/6983 + # social: typeset: From fa79856a4bf25d832e9ca964cc10eb3312e39a99 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 8 Apr 2024 22:52:39 +0000 Subject: [PATCH 348/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 463851ecb6..75cec39e7b 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* 🔧 Update MkDocs, disable cards while I can upgrade to the latest MkDocs Material, that fixes an issue with social cards. PR [#888](https://github.com/tiangolo/sqlmodel/pull/888) by [@tiangolo](https://github.com/tiangolo). * 👷 Add cron to run test once a week on monday. PR [#869](https://github.com/tiangolo/sqlmodel/pull/869) by [@estebanx64](https://github.com/estebanx64). * ⬆️ Upgrade Ruff version and configs. PR [#859](https://github.com/tiangolo/sqlmodel/pull/859) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove Jina QA Bot as it has been discontinued. PR [#840](https://github.com/tiangolo/sqlmodel/pull/840) by [@tiangolo](https://github.com/tiangolo). From 1eb40b1f33aaebd02f03fd1986e242b65c79d573 Mon Sep 17 00:00:00 2001 From: Esteban Maya Date: Mon, 8 Apr 2024 18:07:48 -0500 Subject: [PATCH 349/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20ModelRead=20to?= =?UTF-8?q?=20ModelPublic=20documentation=20and=20examples=20(#885)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- .../fastapi/multiple-models/image03.png | Bin 84929 -> 88924 bytes docs/tutorial/fastapi/multiple-models.md | 14 +++---- docs/tutorial/fastapi/read-one.md | 2 +- docs/tutorial/fastapi/relationships.md | 20 ++++----- docs/tutorial/fastapi/teams.md | 2 +- .../fastapi/app_testing/tutorial001/main.py | 10 ++--- .../app_testing/tutorial001_py310/main.py | 10 ++--- .../app_testing/tutorial001_py39/main.py | 10 ++--- .../tutorial/fastapi/delete/tutorial001.py | 10 ++--- .../fastapi/delete/tutorial001_py310.py | 10 ++--- .../fastapi/delete/tutorial001_py39.py | 10 ++--- .../fastapi/limit_and_offset/tutorial001.py | 8 ++-- .../limit_and_offset/tutorial001_py310.py | 8 ++-- .../limit_and_offset/tutorial001_py39.py | 8 ++-- .../fastapi/multiple_models/tutorial001.py | 6 +-- .../multiple_models/tutorial001_py310.py | 6 +-- .../multiple_models/tutorial001_py39.py | 6 +-- .../fastapi/multiple_models/tutorial002.py | 6 +-- .../multiple_models/tutorial002_py310.py | 6 +-- .../multiple_models/tutorial002_py39.py | 6 +-- .../tutorial/fastapi/read_one/tutorial001.py | 8 ++-- .../fastapi/read_one/tutorial001_py310.py | 8 ++-- .../fastapi/read_one/tutorial001_py39.py | 8 ++-- .../fastapi/relationships/tutorial001.py | 28 ++++++------- .../relationships/tutorial001_py310.py | 28 ++++++------- .../fastapi/relationships/tutorial001_py39.py | 28 ++++++------- .../session_with_dependency/tutorial001.py | 10 ++--- .../tutorial001_py310.py | 10 ++--- .../tutorial001_py39.py | 10 ++--- .../tutorial/fastapi/teams/tutorial001.py | 20 ++++----- .../fastapi/teams/tutorial001_py310.py | 20 ++++----- .../fastapi/teams/tutorial001_py39.py | 20 ++++----- .../tutorial/fastapi/update/tutorial001.py | 10 ++--- .../fastapi/update/tutorial001_py310.py | 10 ++--- .../fastapi/update/tutorial001_py39.py | 10 ++--- .../tutorial/fastapi/update/tutorial002.py | 10 ++--- .../fastapi/update/tutorial002_py310.py | 10 ++--- .../fastapi/update/tutorial002_py39.py | 10 ++--- .../test_delete/test_tutorial001.py | 12 +++--- .../test_delete/test_tutorial001_py310.py | 12 +++--- .../test_delete/test_tutorial001_py39.py | 12 +++--- .../test_limit_and_offset/test_tutorial001.py | 10 ++--- .../test_tutorial001_py310.py | 10 ++--- .../test_tutorial001_py39.py | 10 ++--- .../test_multiple_models/test_tutorial001.py | 8 ++-- .../test_tutorial001_py310.py | 8 ++-- .../test_tutorial001_py39.py | 8 ++-- .../test_multiple_models/test_tutorial002.py | 8 ++-- .../test_tutorial002_py310.py | 8 ++-- .../test_tutorial002_py39.py | 8 ++-- .../test_read_one/test_tutorial001.py | 10 ++--- .../test_read_one/test_tutorial001_py310.py | 10 ++--- .../test_read_one/test_tutorial001_py39.py | 10 ++--- .../test_relationships/test_tutorial001.py | 38 +++++++++--------- .../test_tutorial001_py310.py | 38 +++++++++--------- .../test_tutorial001_py39.py | 38 +++++++++--------- .../test_tutorial001.py | 12 +++--- .../test_tutorial001_py310.py | 12 +++--- .../test_tutorial001_py39.py | 12 +++--- .../test_teams/test_tutorial001.py | 24 +++++------ .../test_teams/test_tutorial001_py310.py | 24 +++++------ .../test_teams/test_tutorial001_py39.py | 24 +++++------ .../test_update/test_tutorial001.py | 12 +++--- .../test_update/test_tutorial001_py310.py | 12 +++--- .../test_update/test_tutorial001_py39.py | 12 +++--- .../test_update/test_tutorial002.py | 12 +++--- .../test_update/test_tutorial002_py310.py | 12 +++--- .../test_update/test_tutorial002_py39.py | 12 +++--- 68 files changed, 427 insertions(+), 427 deletions(-) diff --git a/docs/img/tutorial/fastapi/multiple-models/image03.png b/docs/img/tutorial/fastapi/multiple-models/image03.png index f3830b804543327756110bfa8bcfc3d021920442..796ca5a3703c23e6b6f9721219bf5e434bf6b916 100644 GIT binary patch literal 88924 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 ULDS3y8rDGMUnoCEJTv_GKje!R8~^|S literal 84929 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 diff --git a/docs/tutorial/fastapi/multiple-models.md b/docs/tutorial/fastapi/multiple-models.md index c4db7c6742..41c1ac6498 100644 --- a/docs/tutorial/fastapi/multiple-models.md +++ b/docs/tutorial/fastapi/multiple-models.md @@ -98,7 +98,7 @@ But we also want to have a `HeroCreate` for the data we want to receive when **c * `secret_name`, required * `age`, optional -And we want to have a `HeroRead` with the `id` field, but this time annotated with `id: int`, instead of `id: Optional[int]`, to make it clear that it is required in responses **read** from the clients: +And we want to have a `HeroPublic` with the `id` field, but this time annotated with `id: int`, instead of `id: Optional[int]`, to make it clear that it is required in responses **read** from the clients: * `id`, required * `name`, required @@ -183,9 +183,9 @@ Here's the important detail, and probably the most important feature of **SQLMod This means that the class `Hero` represents a **table** in the database. It is both a **Pydantic** model and a **SQLAlchemy** model. -But `HeroCreate` and `HeroRead` don't have `table = True`. They are only **data models**, they are only **Pydantic** models. They won't be used with the database, but only to declare data schemas for the API (or for other uses). +But `HeroCreate` and `HeroPublic` don't have `table = True`. They are only **data models**, they are only **Pydantic** models. They won't be used with the database, but only to declare data schemas for the API (or for other uses). -This also means that `SQLModel.metadata.create_all()` won't create tables in the database for `HeroCreate` and `HeroRead`, because they don't have `table = True`, which is exactly what we want. 🚀 +This also means that `SQLModel.metadata.create_all()` won't create tables in the database for `HeroCreate` and `HeroPublic`, because they don't have `table = True`, which is exactly what we want. 🚀 /// tip @@ -355,7 +355,7 @@ Then we just `add` it to the **session**, `commit`, and `refresh` it, and finall Because it is just refreshed, it has the `id` field set with a new ID taken from the database. -And now that we return it, FastAPI will validate the data with the `response_model`, which is a `HeroRead`: +And now that we return it, FastAPI will validate the data with the `response_model`, which is a `HeroPublic`: //// tab | Python 3.10+ @@ -743,9 +743,9 @@ As an alternative, we could use `HeroBase` directly in the API code instead of ` On top of that, we could easily decide in the future that we want to receive **more data** when creating a new hero apart from the data in `HeroBase` (for example, a password), and now we already have the class to put those extra fields. -### The `HeroRead` **Data Model** +### The `HeroPublic` **Data Model** -Now let's check the `HeroRead` model. +Now let's check the `HeroPublic` model. This one just declares that the `id` field is required when reading a hero from the API, because a hero read from the API will come from the database, and in the database it will always have an ID. @@ -815,7 +815,7 @@ This one just declares that the `id` field is required when reading a hero from ## Review the Updated Docs UI -The FastAPI code is still the same as above, we still use `Hero`, `HeroCreate`, and `HeroRead`. But now, we define them in a smarter way with inheritance. +The FastAPI code is still the same as above, we still use `Hero`, `HeroCreate`, and `HeroPublic`. But now, we define them in a smarter way with inheritance. So, we can jump to the docs UI right away and see how they look with the updated data. diff --git a/docs/tutorial/fastapi/read-one.md b/docs/tutorial/fastapi/read-one.md index becb2c6c6c..0976961e01 100644 --- a/docs/tutorial/fastapi/read-one.md +++ b/docs/tutorial/fastapi/read-one.md @@ -164,7 +164,7 @@ This will let the client know that they probably made a mistake on their side an Then, if the hero exists, we return it. -And because we are using the `response_model` with `HeroRead`, it will be validated, documented, etc. +And because we are using the `response_model` with `HeroPublic`, it will be validated, documented, etc. //// tab | Python 3.10+ diff --git a/docs/tutorial/fastapi/relationships.md b/docs/tutorial/fastapi/relationships.md index e2a2678903..4087dfca10 100644 --- a/docs/tutorial/fastapi/relationships.md +++ b/docs/tutorial/fastapi/relationships.md @@ -40,9 +40,9 @@ Let's update that. 🤓 First, why is it that we are not getting the related data for each hero and for each team? -It's because we declared the `HeroRead` with only the same base fields of the `HeroBase` plus the `id`. But it doesn't include a field `team` for the **relationship attribute**. +It's because we declared the `HeroPublic` with only the same base fields of the `HeroBase` plus the `id`. But it doesn't include a field `team` for the **relationship attribute**. -And the same way, we declared the `TeamRead` with only the same base fields of the `TeamBase` plus the `id`. But it doesn't include a field `heroes` for the **relationship attribute**. +And the same way, we declared the `TeamPublic` with only the same base fields of the `TeamBase` plus the `id`. But it doesn't include a field `heroes` for the **relationship attribute**. //// tab | Python 3.10+ @@ -146,7 +146,7 @@ And the same way, we declared the `TeamRead` with only the same base fields of t Now, remember that FastAPI uses the `response_model` to validate and **filter** the response data? -In this case, we used `response_model=TeamRead` and `response_model=HeroRead`, so FastAPI will use them to filter the response data, even if we return a **table model** that includes **relationship attributes**: +In this case, we used `response_model=TeamPublic` and `response_model=HeroPublic`, so FastAPI will use them to filter the response data, even if we return a **table model** that includes **relationship attributes**: //// tab | Python 3.10+ @@ -300,7 +300,7 @@ Let's add a couple more **data models** that declare that data so we can use the ## Models with Relationships -Let's add the models `HeroReadWithTeam` and `TeamReadWithHeroes`. +Let's add the models `HeroPublicWithTeam` and `TeamPublicWithHeroes`. We'll add them **after** the other models so that we can easily reference the previous models. @@ -372,11 +372,11 @@ These two models are very **simple in code**, but there's a lot happening here. ### Inheritance and Type Annotations -The `HeroReadWithTeam` **inherits** from `HeroRead`, which means that it will have the **normal fields for reading**, including the required `id` that was declared in `HeroRead`. +The `HeroPublicWithTeam` **inherits** from `HeroPublic`, which means that it will have the **normal fields for reading**, including the required `id` that was declared in `HeroPublic`. -And then it adds the **new field** `team`, which could be `None`, and is declared with the type `TeamRead` with the base fields for reading a team. +And then it adds the **new field** `team`, which could be `None`, and is declared with the type `TeamPublic` with the base fields for reading a team. -Then we do the same for the `TeamReadWithHeroes`, it **inherits** from `TeamRead`, and declares the **new field** `heroes`, which is a list of `HeroRead`. +Then we do the same for the `TeamPublicWithHeroes`, it **inherits** from `TeamPublic`, and declares the **new field** `heroes`, which is a list of `HeroPublic`. ### Data Models Without Relationship Attributes @@ -386,11 +386,11 @@ Instead, here these are only **data models** that will tell FastAPI **which attr ### Reference to Other Models -Also, notice that the field `team` is not declared with this new `TeamReadWithHeroes`, because that would again create that infinite recursion of data. Instead, we declare it with the normal `TeamRead` model. +Also, notice that the field `team` is not declared with this new `TeamPublicWithHeroes`, because that would again create that infinite recursion of data. Instead, we declare it with the normal `TeamPublic` model. -And the same for `TeamReadWithHeroes`, the model used for the new field `heroes` uses `HeroRead` to get only each hero's data. +And the same for `TeamPublicWithHeroes`, the model used for the new field `heroes` uses `HeroPublic` to get only each hero's data. -This also means that, even though we have these two new models, **we still need the previous ones**, `HeroRead` and `TeamRead`, because we need to reference them here (and we are also using them in the rest of the *path operations*). +This also means that, even though we have these two new models, **we still need the previous ones**, `HeroPublic` and `TeamPublic`, because we need to reference them here (and we are also using them in the rest of the *path operations*). ## Update the Path Operations diff --git a/docs/tutorial/fastapi/teams.md b/docs/tutorial/fastapi/teams.md index cbdb4a94ad..9e366f2cc2 100644 --- a/docs/tutorial/fastapi/teams.md +++ b/docs/tutorial/fastapi/teams.md @@ -14,7 +14,7 @@ It's the same process we did for heroes, with a base model, a **table model**, a We have a `TeamBase` **data model**, and from it, we inherit with a `Team` **table model**. -Then we also inherit from the `TeamBase` for the `TeamCreate` and `TeamRead` **data models**. +Then we also inherit from the `TeamBase` for the `TeamCreate` and `TeamPublic` **data models**. And we also create a `TeamUpdate` **data model**. diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py b/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py index 7014a73918..f46d8b078e 100644 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py @@ -18,7 +18,7 @@ class HeroCreate(HeroBase): pass -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -52,7 +52,7 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): db_hero = Hero.model_validate(hero) session.add(db_hero) @@ -61,7 +61,7 @@ def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): return db_hero -@app.get("/heroes/", response_model=List[HeroRead]) +@app.get("/heroes/", response_model=List[HeroPublic]) def read_heroes( *, session: Session = Depends(get_session), @@ -72,7 +72,7 @@ def read_heroes( return heroes -@app.get("/heroes/{hero_id}", response_model=HeroRead) +@app.get("/heroes/{hero_id}", response_model=HeroPublic) def read_hero(*, session: Session = Depends(get_session), hero_id: int): hero = session.get(Hero, hero_id) if not hero: @@ -80,7 +80,7 @@ def read_hero(*, session: Session = Depends(get_session), hero_id: int): return hero -@app.patch("/heroes/{hero_id}", response_model=HeroRead) +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) def update_hero( *, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate ): diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/main.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/main.py index cf1bbb7130..702eba722b 100644 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/main.py +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/main.py @@ -16,7 +16,7 @@ class HeroCreate(HeroBase): pass -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -50,7 +50,7 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): db_hero = Hero.model_validate(hero) session.add(db_hero) @@ -59,7 +59,7 @@ def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): return db_hero -@app.get("/heroes/", response_model=list[HeroRead]) +@app.get("/heroes/", response_model=list[HeroPublic]) def read_heroes( *, session: Session = Depends(get_session), @@ -70,7 +70,7 @@ def read_heroes( return heroes -@app.get("/heroes/{hero_id}", response_model=HeroRead) +@app.get("/heroes/{hero_id}", response_model=HeroPublic) def read_hero(*, session: Session = Depends(get_session), hero_id: int): hero = session.get(Hero, hero_id) if not hero: @@ -78,7 +78,7 @@ def read_hero(*, session: Session = Depends(get_session), hero_id: int): return hero -@app.patch("/heroes/{hero_id}", response_model=HeroRead) +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) def update_hero( *, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate ): diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/main.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/main.py index 9f428ab3e8..efdfd8ee6c 100644 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/main.py +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/main.py @@ -18,7 +18,7 @@ class HeroCreate(HeroBase): pass -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -52,7 +52,7 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): db_hero = Hero.model_validate(hero) session.add(db_hero) @@ -61,7 +61,7 @@ def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): return db_hero -@app.get("/heroes/", response_model=list[HeroRead]) +@app.get("/heroes/", response_model=list[HeroPublic]) def read_heroes( *, session: Session = Depends(get_session), @@ -72,7 +72,7 @@ def read_heroes( return heroes -@app.get("/heroes/{hero_id}", response_model=HeroRead) +@app.get("/heroes/{hero_id}", response_model=HeroPublic) def read_hero(*, session: Session = Depends(get_session), hero_id: int): hero = session.get(Hero, hero_id) if not hero: @@ -80,7 +80,7 @@ def read_hero(*, session: Session = Depends(get_session), hero_id: int): return hero -@app.patch("/heroes/{hero_id}", response_model=HeroRead) +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) def update_hero( *, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate ): diff --git a/docs_src/tutorial/fastapi/delete/tutorial001.py b/docs_src/tutorial/fastapi/delete/tutorial001.py index 532817360a..1871ab1707 100644 --- a/docs_src/tutorial/fastapi/delete/tutorial001.py +++ b/docs_src/tutorial/fastapi/delete/tutorial001.py @@ -18,7 +18,7 @@ class HeroCreate(HeroBase): pass -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -47,7 +47,7 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(hero: HeroCreate): with Session(engine) as session: db_hero = Hero.model_validate(hero) @@ -57,14 +57,14 @@ def create_hero(hero: HeroCreate): return db_hero -@app.get("/heroes/", response_model=List[HeroRead]) +@app.get("/heroes/", response_model=List[HeroPublic]) def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): with Session(engine) as session: heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() return heroes -@app.get("/heroes/{hero_id}", response_model=HeroRead) +@app.get("/heroes/{hero_id}", response_model=HeroPublic) def read_hero(hero_id: int): with Session(engine) as session: hero = session.get(Hero, hero_id) @@ -73,7 +73,7 @@ def read_hero(hero_id: int): return hero -@app.patch("/heroes/{hero_id}", response_model=HeroRead) +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) def update_hero(hero_id: int, hero: HeroUpdate): with Session(engine) as session: db_hero = session.get(Hero, hero_id) diff --git a/docs_src/tutorial/fastapi/delete/tutorial001_py310.py b/docs_src/tutorial/fastapi/delete/tutorial001_py310.py index 45e2e1d515..0dd0c889fa 100644 --- a/docs_src/tutorial/fastapi/delete/tutorial001_py310.py +++ b/docs_src/tutorial/fastapi/delete/tutorial001_py310.py @@ -16,7 +16,7 @@ class HeroCreate(HeroBase): pass -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -45,7 +45,7 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(hero: HeroCreate): with Session(engine) as session: db_hero = Hero.model_validate(hero) @@ -55,14 +55,14 @@ def create_hero(hero: HeroCreate): return db_hero -@app.get("/heroes/", response_model=list[HeroRead]) +@app.get("/heroes/", response_model=list[HeroPublic]) def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): with Session(engine) as session: heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() return heroes -@app.get("/heroes/{hero_id}", response_model=HeroRead) +@app.get("/heroes/{hero_id}", response_model=HeroPublic) def read_hero(hero_id: int): with Session(engine) as session: hero = session.get(Hero, hero_id) @@ -71,7 +71,7 @@ def read_hero(hero_id: int): return hero -@app.patch("/heroes/{hero_id}", response_model=HeroRead) +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) def update_hero(hero_id: int, hero: HeroUpdate): with Session(engine) as session: db_hero = session.get(Hero, hero_id) diff --git a/docs_src/tutorial/fastapi/delete/tutorial001_py39.py b/docs_src/tutorial/fastapi/delete/tutorial001_py39.py index 12f6bc3f9b..9ab3056c82 100644 --- a/docs_src/tutorial/fastapi/delete/tutorial001_py39.py +++ b/docs_src/tutorial/fastapi/delete/tutorial001_py39.py @@ -18,7 +18,7 @@ class HeroCreate(HeroBase): pass -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -47,7 +47,7 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(hero: HeroCreate): with Session(engine) as session: db_hero = Hero.model_validate(hero) @@ -57,14 +57,14 @@ def create_hero(hero: HeroCreate): return db_hero -@app.get("/heroes/", response_model=list[HeroRead]) +@app.get("/heroes/", response_model=list[HeroPublic]) def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): with Session(engine) as session: heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() return heroes -@app.get("/heroes/{hero_id}", response_model=HeroRead) +@app.get("/heroes/{hero_id}", response_model=HeroPublic) def read_hero(hero_id: int): with Session(engine) as session: hero = session.get(Hero, hero_id) @@ -73,7 +73,7 @@ def read_hero(hero_id: int): return hero -@app.patch("/heroes/{hero_id}", response_model=HeroRead) +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) def update_hero(hero_id: int, hero: HeroUpdate): with Session(engine) as session: db_hero = session.get(Hero, hero_id) diff --git a/docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py b/docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py index 2352f39022..ccf3d77036 100644 --- a/docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py +++ b/docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py @@ -18,7 +18,7 @@ class HeroCreate(HeroBase): pass -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -41,7 +41,7 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(hero: HeroCreate): with Session(engine) as session: db_hero = Hero.model_validate(hero) @@ -51,14 +51,14 @@ def create_hero(hero: HeroCreate): return db_hero -@app.get("/heroes/", response_model=List[HeroRead]) +@app.get("/heroes/", response_model=List[HeroPublic]) def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): with Session(engine) as session: heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() return heroes -@app.get("/heroes/{hero_id}", response_model=HeroRead) +@app.get("/heroes/{hero_id}", response_model=HeroPublic) def read_hero(hero_id: int): with Session(engine) as session: hero = session.get(Hero, hero_id) diff --git a/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py310.py b/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py310.py index ad8ff95e3a..3402d4045e 100644 --- a/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py310.py +++ b/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py310.py @@ -16,7 +16,7 @@ class HeroCreate(HeroBase): pass -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -39,7 +39,7 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(hero: HeroCreate): with Session(engine) as session: db_hero = Hero.model_validate(hero) @@ -49,14 +49,14 @@ def create_hero(hero: HeroCreate): return db_hero -@app.get("/heroes/", response_model=list[HeroRead]) +@app.get("/heroes/", response_model=list[HeroPublic]) def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): with Session(engine) as session: heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() return heroes -@app.get("/heroes/{hero_id}", response_model=HeroRead) +@app.get("/heroes/{hero_id}", response_model=HeroPublic) def read_hero(hero_id: int): with Session(engine) as session: hero = session.get(Hero, hero_id) diff --git a/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py39.py b/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py39.py index b1f7cdcb6a..3d223f3290 100644 --- a/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py39.py +++ b/docs_src/tutorial/fastapi/limit_and_offset/tutorial001_py39.py @@ -18,7 +18,7 @@ class HeroCreate(HeroBase): pass -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -41,7 +41,7 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(hero: HeroCreate): with Session(engine) as session: db_hero = Hero.model_validate(hero) @@ -51,14 +51,14 @@ def create_hero(hero: HeroCreate): return db_hero -@app.get("/heroes/", response_model=list[HeroRead]) +@app.get("/heroes/", response_model=list[HeroPublic]) def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): with Session(engine) as session: heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() return heroes -@app.get("/heroes/{hero_id}", response_model=HeroRead) +@app.get("/heroes/{hero_id}", response_model=HeroPublic) def read_hero(hero_id: int): with Session(engine) as session: hero = session.get(Hero, hero_id) diff --git a/docs_src/tutorial/fastapi/multiple_models/tutorial001.py b/docs_src/tutorial/fastapi/multiple_models/tutorial001.py index 7f59ac6a1d..42ac8051b1 100644 --- a/docs_src/tutorial/fastapi/multiple_models/tutorial001.py +++ b/docs_src/tutorial/fastapi/multiple_models/tutorial001.py @@ -17,7 +17,7 @@ class HeroCreate(SQLModel): age: Optional[int] = None -class HeroRead(SQLModel): +class HeroPublic(SQLModel): id: int name: str secret_name: str @@ -43,7 +43,7 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(hero: HeroCreate): with Session(engine) as session: db_hero = Hero.model_validate(hero) @@ -53,7 +53,7 @@ def create_hero(hero: HeroCreate): return db_hero -@app.get("/heroes/", response_model=List[HeroRead]) +@app.get("/heroes/", response_model=List[HeroPublic]) def read_heroes(): with Session(engine) as session: heroes = session.exec(select(Hero)).all() diff --git a/docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py b/docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py index ff12eff55c..b8dc44d981 100644 --- a/docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py +++ b/docs_src/tutorial/fastapi/multiple_models/tutorial001_py310.py @@ -15,7 +15,7 @@ class HeroCreate(SQLModel): age: int | None = None -class HeroRead(SQLModel): +class HeroPublic(SQLModel): id: int name: str secret_name: str @@ -41,7 +41,7 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(hero: HeroCreate): with Session(engine) as session: db_hero = Hero.model_validate(hero) @@ -51,7 +51,7 @@ def create_hero(hero: HeroCreate): return db_hero -@app.get("/heroes/", response_model=list[HeroRead]) +@app.get("/heroes/", response_model=list[HeroPublic]) def read_heroes(): with Session(engine) as session: heroes = session.exec(select(Hero)).all() diff --git a/docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py b/docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py index 977a1ac8db..c6be23d441 100644 --- a/docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py +++ b/docs_src/tutorial/fastapi/multiple_models/tutorial001_py39.py @@ -17,7 +17,7 @@ class HeroCreate(SQLModel): age: Optional[int] = None -class HeroRead(SQLModel): +class HeroPublic(SQLModel): id: int name: str secret_name: str @@ -43,7 +43,7 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(hero: HeroCreate): with Session(engine) as session: db_hero = Hero.model_validate(hero) @@ -53,7 +53,7 @@ def create_hero(hero: HeroCreate): return db_hero -@app.get("/heroes/", response_model=list[HeroRead]) +@app.get("/heroes/", response_model=list[HeroPublic]) def read_heroes(): with Session(engine) as session: heroes = session.exec(select(Hero)).all() diff --git a/docs_src/tutorial/fastapi/multiple_models/tutorial002.py b/docs_src/tutorial/fastapi/multiple_models/tutorial002.py index fffbe72496..79c71f1a2e 100644 --- a/docs_src/tutorial/fastapi/multiple_models/tutorial002.py +++ b/docs_src/tutorial/fastapi/multiple_models/tutorial002.py @@ -18,7 +18,7 @@ class HeroCreate(HeroBase): pass -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -41,7 +41,7 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(hero: HeroCreate): with Session(engine) as session: db_hero = Hero.model_validate(hero) @@ -51,7 +51,7 @@ def create_hero(hero: HeroCreate): return db_hero -@app.get("/heroes/", response_model=List[HeroRead]) +@app.get("/heroes/", response_model=List[HeroPublic]) def read_heroes(): with Session(engine) as session: heroes = session.exec(select(Hero)).all() diff --git a/docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py b/docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py index 7373edff5e..79e7447b53 100644 --- a/docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py +++ b/docs_src/tutorial/fastapi/multiple_models/tutorial002_py310.py @@ -16,7 +16,7 @@ class HeroCreate(HeroBase): pass -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -39,7 +39,7 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(hero: HeroCreate): with Session(engine) as session: db_hero = Hero.model_validate(hero) @@ -49,7 +49,7 @@ def create_hero(hero: HeroCreate): return db_hero -@app.get("/heroes/", response_model=list[HeroRead]) +@app.get("/heroes/", response_model=list[HeroPublic]) def read_heroes(): with Session(engine) as session: heroes = session.exec(select(Hero)).all() diff --git a/docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py b/docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py index 1b4a512520..77093bcc7c 100644 --- a/docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py +++ b/docs_src/tutorial/fastapi/multiple_models/tutorial002_py39.py @@ -18,7 +18,7 @@ class HeroCreate(HeroBase): pass -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -41,7 +41,7 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(hero: HeroCreate): with Session(engine) as session: db_hero = Hero.model_validate(hero) @@ -51,7 +51,7 @@ def create_hero(hero: HeroCreate): return db_hero -@app.get("/heroes/", response_model=list[HeroRead]) +@app.get("/heroes/", response_model=list[HeroPublic]) def read_heroes(): with Session(engine) as session: heroes = session.exec(select(Hero)).all() diff --git a/docs_src/tutorial/fastapi/read_one/tutorial001.py b/docs_src/tutorial/fastapi/read_one/tutorial001.py index f18426e74c..a39945173b 100644 --- a/docs_src/tutorial/fastapi/read_one/tutorial001.py +++ b/docs_src/tutorial/fastapi/read_one/tutorial001.py @@ -18,7 +18,7 @@ class HeroCreate(HeroBase): pass -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -41,7 +41,7 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(hero: HeroCreate): with Session(engine) as session: db_hero = Hero.model_validate(hero) @@ -51,14 +51,14 @@ def create_hero(hero: HeroCreate): return db_hero -@app.get("/heroes/", response_model=List[HeroRead]) +@app.get("/heroes/", response_model=List[HeroPublic]) def read_heroes(): with Session(engine) as session: heroes = session.exec(select(Hero)).all() return heroes -@app.get("/heroes/{hero_id}", response_model=HeroRead) +@app.get("/heroes/{hero_id}", response_model=HeroPublic) def read_hero(hero_id: int): with Session(engine) as session: hero = session.get(Hero, hero_id) diff --git a/docs_src/tutorial/fastapi/read_one/tutorial001_py310.py b/docs_src/tutorial/fastapi/read_one/tutorial001_py310.py index e8c7d49b99..1a4628137c 100644 --- a/docs_src/tutorial/fastapi/read_one/tutorial001_py310.py +++ b/docs_src/tutorial/fastapi/read_one/tutorial001_py310.py @@ -16,7 +16,7 @@ class HeroCreate(HeroBase): pass -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -39,7 +39,7 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(hero: HeroCreate): with Session(engine) as session: db_hero = Hero.model_validate(hero) @@ -49,14 +49,14 @@ def create_hero(hero: HeroCreate): return db_hero -@app.get("/heroes/", response_model=list[HeroRead]) +@app.get("/heroes/", response_model=list[HeroPublic]) def read_heroes(): with Session(engine) as session: heroes = session.exec(select(Hero)).all() return heroes -@app.get("/heroes/{hero_id}", response_model=HeroRead) +@app.get("/heroes/{hero_id}", response_model=HeroPublic) def read_hero(hero_id: int): with Session(engine) as session: hero = session.get(Hero, hero_id) diff --git a/docs_src/tutorial/fastapi/read_one/tutorial001_py39.py b/docs_src/tutorial/fastapi/read_one/tutorial001_py39.py index 4dc5702fb6..9ac0a65088 100644 --- a/docs_src/tutorial/fastapi/read_one/tutorial001_py39.py +++ b/docs_src/tutorial/fastapi/read_one/tutorial001_py39.py @@ -18,7 +18,7 @@ class HeroCreate(HeroBase): pass -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -41,7 +41,7 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(hero: HeroCreate): with Session(engine) as session: db_hero = Hero.model_validate(hero) @@ -51,14 +51,14 @@ def create_hero(hero: HeroCreate): return db_hero -@app.get("/heroes/", response_model=list[HeroRead]) +@app.get("/heroes/", response_model=list[HeroPublic]) def read_heroes(): with Session(engine) as session: heroes = session.exec(select(Hero)).all() return heroes -@app.get("/heroes/{hero_id}", response_model=HeroRead) +@app.get("/heroes/{hero_id}", response_model=HeroPublic) def read_hero(hero_id: int): with Session(engine) as session: hero = session.get(Hero, hero_id) diff --git a/docs_src/tutorial/fastapi/relationships/tutorial001.py b/docs_src/tutorial/fastapi/relationships/tutorial001.py index 51339e2a20..ac8a557cfc 100644 --- a/docs_src/tutorial/fastapi/relationships/tutorial001.py +++ b/docs_src/tutorial/fastapi/relationships/tutorial001.py @@ -19,7 +19,7 @@ class TeamCreate(TeamBase): pass -class TeamRead(TeamBase): +class TeamPublic(TeamBase): id: int @@ -43,7 +43,7 @@ class Hero(HeroBase, table=True): team: Optional[Team] = Relationship(back_populates="heroes") -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -58,12 +58,12 @@ class HeroUpdate(SQLModel): team_id: Optional[int] = None -class HeroReadWithTeam(HeroRead): - team: Optional[TeamRead] = None +class HeroPublicWithTeam(HeroPublic): + team: Optional[TeamPublic] = None -class TeamReadWithHeroes(TeamRead): - heroes: List[HeroRead] = [] +class TeamPublicWithHeroes(TeamPublic): + heroes: List[HeroPublic] = [] sqlite_file_name = "database.db" @@ -90,7 +90,7 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): db_hero = Hero.model_validate(hero) session.add(db_hero) @@ -99,7 +99,7 @@ def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): return db_hero -@app.get("/heroes/", response_model=List[HeroRead]) +@app.get("/heroes/", response_model=List[HeroPublic]) def read_heroes( *, session: Session = Depends(get_session), @@ -110,7 +110,7 @@ def read_heroes( return heroes -@app.get("/heroes/{hero_id}", response_model=HeroReadWithTeam) +@app.get("/heroes/{hero_id}", response_model=HeroPublicWithTeam) def read_hero(*, session: Session = Depends(get_session), hero_id: int): hero = session.get(Hero, hero_id) if not hero: @@ -118,7 +118,7 @@ def read_hero(*, session: Session = Depends(get_session), hero_id: int): return hero -@app.patch("/heroes/{hero_id}", response_model=HeroRead) +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) def update_hero( *, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate ): @@ -144,7 +144,7 @@ def delete_hero(*, session: Session = Depends(get_session), hero_id: int): return {"ok": True} -@app.post("/teams/", response_model=TeamRead) +@app.post("/teams/", response_model=TeamPublic) def create_team(*, session: Session = Depends(get_session), team: TeamCreate): db_team = Team.model_validate(team) session.add(db_team) @@ -153,7 +153,7 @@ def create_team(*, session: Session = Depends(get_session), team: TeamCreate): return db_team -@app.get("/teams/", response_model=List[TeamRead]) +@app.get("/teams/", response_model=List[TeamPublic]) def read_teams( *, session: Session = Depends(get_session), @@ -164,7 +164,7 @@ def read_teams( return teams -@app.get("/teams/{team_id}", response_model=TeamReadWithHeroes) +@app.get("/teams/{team_id}", response_model=TeamPublicWithHeroes) def read_team(*, team_id: int, session: Session = Depends(get_session)): team = session.get(Team, team_id) if not team: @@ -172,7 +172,7 @@ def read_team(*, team_id: int, session: Session = Depends(get_session)): return team -@app.patch("/teams/{team_id}", response_model=TeamRead) +@app.patch("/teams/{team_id}", response_model=TeamPublic) def update_team( *, session: Session = Depends(get_session), diff --git a/docs_src/tutorial/fastapi/relationships/tutorial001_py310.py b/docs_src/tutorial/fastapi/relationships/tutorial001_py310.py index 35257bd513..5110b158b0 100644 --- a/docs_src/tutorial/fastapi/relationships/tutorial001_py310.py +++ b/docs_src/tutorial/fastapi/relationships/tutorial001_py310.py @@ -17,7 +17,7 @@ class TeamCreate(TeamBase): pass -class TeamRead(TeamBase): +class TeamPublic(TeamBase): id: int @@ -41,7 +41,7 @@ class Hero(HeroBase, table=True): team: Team | None = Relationship(back_populates="heroes") -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -56,12 +56,12 @@ class HeroUpdate(SQLModel): team_id: int | None = None -class HeroReadWithTeam(HeroRead): - team: TeamRead | None = None +class HeroPublicWithTeam(HeroPublic): + team: TeamPublic | None = None -class TeamReadWithHeroes(TeamRead): - heroes: list[HeroRead] = [] +class TeamPublicWithHeroes(TeamPublic): + heroes: list[HeroPublic] = [] sqlite_file_name = "database.db" @@ -88,7 +88,7 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): db_hero = Hero.model_validate(hero) session.add(db_hero) @@ -97,7 +97,7 @@ def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): return db_hero -@app.get("/heroes/", response_model=list[HeroRead]) +@app.get("/heroes/", response_model=list[HeroPublic]) def read_heroes( *, session: Session = Depends(get_session), @@ -108,7 +108,7 @@ def read_heroes( return heroes -@app.get("/heroes/{hero_id}", response_model=HeroReadWithTeam) +@app.get("/heroes/{hero_id}", response_model=HeroPublicWithTeam) def read_hero(*, session: Session = Depends(get_session), hero_id: int): hero = session.get(Hero, hero_id) if not hero: @@ -116,7 +116,7 @@ def read_hero(*, session: Session = Depends(get_session), hero_id: int): return hero -@app.patch("/heroes/{hero_id}", response_model=HeroRead) +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) def update_hero( *, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate ): @@ -142,7 +142,7 @@ def delete_hero(*, session: Session = Depends(get_session), hero_id: int): return {"ok": True} -@app.post("/teams/", response_model=TeamRead) +@app.post("/teams/", response_model=TeamPublic) def create_team(*, session: Session = Depends(get_session), team: TeamCreate): db_team = Team.model_validate(team) session.add(db_team) @@ -151,7 +151,7 @@ def create_team(*, session: Session = Depends(get_session), team: TeamCreate): return db_team -@app.get("/teams/", response_model=list[TeamRead]) +@app.get("/teams/", response_model=list[TeamPublic]) def read_teams( *, session: Session = Depends(get_session), @@ -162,7 +162,7 @@ def read_teams( return teams -@app.get("/teams/{team_id}", response_model=TeamReadWithHeroes) +@app.get("/teams/{team_id}", response_model=TeamPublicWithHeroes) def read_team(*, team_id: int, session: Session = Depends(get_session)): team = session.get(Team, team_id) if not team: @@ -170,7 +170,7 @@ def read_team(*, team_id: int, session: Session = Depends(get_session)): return team -@app.patch("/teams/{team_id}", response_model=TeamRead) +@app.patch("/teams/{team_id}", response_model=TeamPublic) def update_team( *, session: Session = Depends(get_session), diff --git a/docs_src/tutorial/fastapi/relationships/tutorial001_py39.py b/docs_src/tutorial/fastapi/relationships/tutorial001_py39.py index 6ceae130a3..a4e953c4d7 100644 --- a/docs_src/tutorial/fastapi/relationships/tutorial001_py39.py +++ b/docs_src/tutorial/fastapi/relationships/tutorial001_py39.py @@ -19,7 +19,7 @@ class TeamCreate(TeamBase): pass -class TeamRead(TeamBase): +class TeamPublic(TeamBase): id: int @@ -43,7 +43,7 @@ class Hero(HeroBase, table=True): team: Optional[Team] = Relationship(back_populates="heroes") -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -58,12 +58,12 @@ class HeroUpdate(SQLModel): team_id: Optional[int] = None -class HeroReadWithTeam(HeroRead): - team: Optional[TeamRead] = None +class HeroPublicWithTeam(HeroPublic): + team: Optional[TeamPublic] = None -class TeamReadWithHeroes(TeamRead): - heroes: list[HeroRead] = [] +class TeamPublicWithHeroes(TeamPublic): + heroes: list[HeroPublic] = [] sqlite_file_name = "database.db" @@ -90,7 +90,7 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): db_hero = Hero.model_validate(hero) session.add(db_hero) @@ -99,7 +99,7 @@ def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): return db_hero -@app.get("/heroes/", response_model=list[HeroRead]) +@app.get("/heroes/", response_model=list[HeroPublic]) def read_heroes( *, session: Session = Depends(get_session), @@ -110,7 +110,7 @@ def read_heroes( return heroes -@app.get("/heroes/{hero_id}", response_model=HeroReadWithTeam) +@app.get("/heroes/{hero_id}", response_model=HeroPublicWithTeam) def read_hero(*, session: Session = Depends(get_session), hero_id: int): hero = session.get(Hero, hero_id) if not hero: @@ -118,7 +118,7 @@ def read_hero(*, session: Session = Depends(get_session), hero_id: int): return hero -@app.patch("/heroes/{hero_id}", response_model=HeroRead) +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) def update_hero( *, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate ): @@ -144,7 +144,7 @@ def delete_hero(*, session: Session = Depends(get_session), hero_id: int): return {"ok": True} -@app.post("/teams/", response_model=TeamRead) +@app.post("/teams/", response_model=TeamPublic) def create_team(*, session: Session = Depends(get_session), team: TeamCreate): db_team = Team.model_validate(team) session.add(db_team) @@ -153,7 +153,7 @@ def create_team(*, session: Session = Depends(get_session), team: TeamCreate): return db_team -@app.get("/teams/", response_model=list[TeamRead]) +@app.get("/teams/", response_model=list[TeamPublic]) def read_teams( *, session: Session = Depends(get_session), @@ -164,7 +164,7 @@ def read_teams( return teams -@app.get("/teams/{team_id}", response_model=TeamReadWithHeroes) +@app.get("/teams/{team_id}", response_model=TeamPublicWithHeroes) def read_team(*, team_id: int, session: Session = Depends(get_session)): team = session.get(Team, team_id) if not team: @@ -172,7 +172,7 @@ def read_team(*, team_id: int, session: Session = Depends(get_session)): return team -@app.patch("/teams/{team_id}", response_model=TeamRead) +@app.patch("/teams/{team_id}", response_model=TeamPublic) def update_team( *, session: Session = Depends(get_session), diff --git a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py index 7014a73918..f46d8b078e 100644 --- a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py +++ b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py @@ -18,7 +18,7 @@ class HeroCreate(HeroBase): pass -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -52,7 +52,7 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): db_hero = Hero.model_validate(hero) session.add(db_hero) @@ -61,7 +61,7 @@ def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): return db_hero -@app.get("/heroes/", response_model=List[HeroRead]) +@app.get("/heroes/", response_model=List[HeroPublic]) def read_heroes( *, session: Session = Depends(get_session), @@ -72,7 +72,7 @@ def read_heroes( return heroes -@app.get("/heroes/{hero_id}", response_model=HeroRead) +@app.get("/heroes/{hero_id}", response_model=HeroPublic) def read_hero(*, session: Session = Depends(get_session), hero_id: int): hero = session.get(Hero, hero_id) if not hero: @@ -80,7 +80,7 @@ def read_hero(*, session: Session = Depends(get_session), hero_id: int): return hero -@app.patch("/heroes/{hero_id}", response_model=HeroRead) +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) def update_hero( *, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate ): diff --git a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py index cf1bbb7130..702eba722b 100644 --- a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py +++ b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py @@ -16,7 +16,7 @@ class HeroCreate(HeroBase): pass -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -50,7 +50,7 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): db_hero = Hero.model_validate(hero) session.add(db_hero) @@ -59,7 +59,7 @@ def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): return db_hero -@app.get("/heroes/", response_model=list[HeroRead]) +@app.get("/heroes/", response_model=list[HeroPublic]) def read_heroes( *, session: Session = Depends(get_session), @@ -70,7 +70,7 @@ def read_heroes( return heroes -@app.get("/heroes/{hero_id}", response_model=HeroRead) +@app.get("/heroes/{hero_id}", response_model=HeroPublic) def read_hero(*, session: Session = Depends(get_session), hero_id: int): hero = session.get(Hero, hero_id) if not hero: @@ -78,7 +78,7 @@ def read_hero(*, session: Session = Depends(get_session), hero_id: int): return hero -@app.patch("/heroes/{hero_id}", response_model=HeroRead) +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) def update_hero( *, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate ): diff --git a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py index 9f428ab3e8..efdfd8ee6c 100644 --- a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py +++ b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py @@ -18,7 +18,7 @@ class HeroCreate(HeroBase): pass -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -52,7 +52,7 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): db_hero = Hero.model_validate(hero) session.add(db_hero) @@ -61,7 +61,7 @@ def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): return db_hero -@app.get("/heroes/", response_model=list[HeroRead]) +@app.get("/heroes/", response_model=list[HeroPublic]) def read_heroes( *, session: Session = Depends(get_session), @@ -72,7 +72,7 @@ def read_heroes( return heroes -@app.get("/heroes/{hero_id}", response_model=HeroRead) +@app.get("/heroes/{hero_id}", response_model=HeroPublic) def read_hero(*, session: Session = Depends(get_session), hero_id: int): hero = session.get(Hero, hero_id) if not hero: @@ -80,7 +80,7 @@ def read_hero(*, session: Session = Depends(get_session), hero_id: int): return hero -@app.patch("/heroes/{hero_id}", response_model=HeroRead) +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) def update_hero( *, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate ): diff --git a/docs_src/tutorial/fastapi/teams/tutorial001.py b/docs_src/tutorial/fastapi/teams/tutorial001.py index 785c525918..4289221ff5 100644 --- a/docs_src/tutorial/fastapi/teams/tutorial001.py +++ b/docs_src/tutorial/fastapi/teams/tutorial001.py @@ -19,7 +19,7 @@ class TeamCreate(TeamBase): pass -class TeamRead(TeamBase): +class TeamPublic(TeamBase): id: int @@ -42,7 +42,7 @@ class Hero(HeroBase, table=True): team: Optional[Team] = Relationship(back_populates="heroes") -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -81,7 +81,7 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): db_hero = Hero.model_validate(hero) session.add(db_hero) @@ -90,7 +90,7 @@ def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): return db_hero -@app.get("/heroes/", response_model=List[HeroRead]) +@app.get("/heroes/", response_model=List[HeroPublic]) def read_heroes( *, session: Session = Depends(get_session), @@ -101,7 +101,7 @@ def read_heroes( return heroes -@app.get("/heroes/{hero_id}", response_model=HeroRead) +@app.get("/heroes/{hero_id}", response_model=HeroPublic) def read_hero(*, session: Session = Depends(get_session), hero_id: int): hero = session.get(Hero, hero_id) if not hero: @@ -109,7 +109,7 @@ def read_hero(*, session: Session = Depends(get_session), hero_id: int): return hero -@app.patch("/heroes/{hero_id}", response_model=HeroRead) +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) def update_hero( *, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate ): @@ -135,7 +135,7 @@ def delete_hero(*, session: Session = Depends(get_session), hero_id: int): return {"ok": True} -@app.post("/teams/", response_model=TeamRead) +@app.post("/teams/", response_model=TeamPublic) def create_team(*, session: Session = Depends(get_session), team: TeamCreate): db_team = Team.model_validate(team) session.add(db_team) @@ -144,7 +144,7 @@ def create_team(*, session: Session = Depends(get_session), team: TeamCreate): return db_team -@app.get("/teams/", response_model=List[TeamRead]) +@app.get("/teams/", response_model=List[TeamPublic]) def read_teams( *, session: Session = Depends(get_session), @@ -155,7 +155,7 @@ def read_teams( return teams -@app.get("/teams/{team_id}", response_model=TeamRead) +@app.get("/teams/{team_id}", response_model=TeamPublic) def read_team(*, team_id: int, session: Session = Depends(get_session)): team = session.get(Team, team_id) if not team: @@ -163,7 +163,7 @@ def read_team(*, team_id: int, session: Session = Depends(get_session)): return team -@app.patch("/teams/{team_id}", response_model=TeamRead) +@app.patch("/teams/{team_id}", response_model=TeamPublic) def update_team( *, session: Session = Depends(get_session), diff --git a/docs_src/tutorial/fastapi/teams/tutorial001_py310.py b/docs_src/tutorial/fastapi/teams/tutorial001_py310.py index dea4bd8a9b..630ae4f98a 100644 --- a/docs_src/tutorial/fastapi/teams/tutorial001_py310.py +++ b/docs_src/tutorial/fastapi/teams/tutorial001_py310.py @@ -17,7 +17,7 @@ class TeamCreate(TeamBase): pass -class TeamRead(TeamBase): +class TeamPublic(TeamBase): id: int @@ -40,7 +40,7 @@ class Hero(HeroBase, table=True): team: Team | None = Relationship(back_populates="heroes") -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -79,7 +79,7 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): db_hero = Hero.model_validate(hero) session.add(db_hero) @@ -88,7 +88,7 @@ def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): return db_hero -@app.get("/heroes/", response_model=list[HeroRead]) +@app.get("/heroes/", response_model=list[HeroPublic]) def read_heroes( *, session: Session = Depends(get_session), @@ -99,7 +99,7 @@ def read_heroes( return heroes -@app.get("/heroes/{hero_id}", response_model=HeroRead) +@app.get("/heroes/{hero_id}", response_model=HeroPublic) def read_hero(*, session: Session = Depends(get_session), hero_id: int): hero = session.get(Hero, hero_id) if not hero: @@ -107,7 +107,7 @@ def read_hero(*, session: Session = Depends(get_session), hero_id: int): return hero -@app.patch("/heroes/{hero_id}", response_model=HeroRead) +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) def update_hero( *, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate ): @@ -133,7 +133,7 @@ def delete_hero(*, session: Session = Depends(get_session), hero_id: int): return {"ok": True} -@app.post("/teams/", response_model=TeamRead) +@app.post("/teams/", response_model=TeamPublic) def create_team(*, session: Session = Depends(get_session), team: TeamCreate): db_team = Team.model_validate(team) session.add(db_team) @@ -142,7 +142,7 @@ def create_team(*, session: Session = Depends(get_session), team: TeamCreate): return db_team -@app.get("/teams/", response_model=list[TeamRead]) +@app.get("/teams/", response_model=list[TeamPublic]) def read_teams( *, session: Session = Depends(get_session), @@ -153,7 +153,7 @@ def read_teams( return teams -@app.get("/teams/{team_id}", response_model=TeamRead) +@app.get("/teams/{team_id}", response_model=TeamPublic) def read_team(*, team_id: int, session: Session = Depends(get_session)): team = session.get(Team, team_id) if not team: @@ -161,7 +161,7 @@ def read_team(*, team_id: int, session: Session = Depends(get_session)): return team -@app.patch("/teams/{team_id}", response_model=TeamRead) +@app.patch("/teams/{team_id}", response_model=TeamPublic) def update_team( *, session: Session = Depends(get_session), diff --git a/docs_src/tutorial/fastapi/teams/tutorial001_py39.py b/docs_src/tutorial/fastapi/teams/tutorial001_py39.py index cc6429adcf..661e435373 100644 --- a/docs_src/tutorial/fastapi/teams/tutorial001_py39.py +++ b/docs_src/tutorial/fastapi/teams/tutorial001_py39.py @@ -19,7 +19,7 @@ class TeamCreate(TeamBase): pass -class TeamRead(TeamBase): +class TeamPublic(TeamBase): id: int @@ -42,7 +42,7 @@ class Hero(HeroBase, table=True): team: Optional[Team] = Relationship(back_populates="heroes") -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -81,7 +81,7 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): db_hero = Hero.model_validate(hero) session.add(db_hero) @@ -90,7 +90,7 @@ def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): return db_hero -@app.get("/heroes/", response_model=list[HeroRead]) +@app.get("/heroes/", response_model=list[HeroPublic]) def read_heroes( *, session: Session = Depends(get_session), @@ -101,7 +101,7 @@ def read_heroes( return heroes -@app.get("/heroes/{hero_id}", response_model=HeroRead) +@app.get("/heroes/{hero_id}", response_model=HeroPublic) def read_hero(*, session: Session = Depends(get_session), hero_id: int): hero = session.get(Hero, hero_id) if not hero: @@ -109,7 +109,7 @@ def read_hero(*, session: Session = Depends(get_session), hero_id: int): return hero -@app.patch("/heroes/{hero_id}", response_model=HeroRead) +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) def update_hero( *, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate ): @@ -135,7 +135,7 @@ def delete_hero(*, session: Session = Depends(get_session), hero_id: int): return {"ok": True} -@app.post("/teams/", response_model=TeamRead) +@app.post("/teams/", response_model=TeamPublic) def create_team(*, session: Session = Depends(get_session), team: TeamCreate): db_team = Team.model_validate(team) session.add(db_team) @@ -144,7 +144,7 @@ def create_team(*, session: Session = Depends(get_session), team: TeamCreate): return db_team -@app.get("/teams/", response_model=list[TeamRead]) +@app.get("/teams/", response_model=list[TeamPublic]) def read_teams( *, session: Session = Depends(get_session), @@ -155,7 +155,7 @@ def read_teams( return teams -@app.get("/teams/{team_id}", response_model=TeamRead) +@app.get("/teams/{team_id}", response_model=TeamPublic) def read_team(*, team_id: int, session: Session = Depends(get_session)): team = session.get(Team, team_id) if not team: @@ -163,7 +163,7 @@ def read_team(*, team_id: int, session: Session = Depends(get_session)): return team -@app.patch("/teams/{team_id}", response_model=TeamRead) +@app.patch("/teams/{team_id}", response_model=TeamPublic) def update_team( *, session: Session = Depends(get_session), diff --git a/docs_src/tutorial/fastapi/update/tutorial001.py b/docs_src/tutorial/fastapi/update/tutorial001.py index feab25cc13..6a02f6c015 100644 --- a/docs_src/tutorial/fastapi/update/tutorial001.py +++ b/docs_src/tutorial/fastapi/update/tutorial001.py @@ -18,7 +18,7 @@ class HeroCreate(HeroBase): pass -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -47,7 +47,7 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(hero: HeroCreate): with Session(engine) as session: db_hero = Hero.model_validate(hero) @@ -57,14 +57,14 @@ def create_hero(hero: HeroCreate): return db_hero -@app.get("/heroes/", response_model=List[HeroRead]) +@app.get("/heroes/", response_model=List[HeroPublic]) def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): with Session(engine) as session: heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() return heroes -@app.get("/heroes/{hero_id}", response_model=HeroRead) +@app.get("/heroes/{hero_id}", response_model=HeroPublic) def read_hero(hero_id: int): with Session(engine) as session: hero = session.get(Hero, hero_id) @@ -73,7 +73,7 @@ def read_hero(hero_id: int): return hero -@app.patch("/heroes/{hero_id}", response_model=HeroRead) +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) def update_hero(hero_id: int, hero: HeroUpdate): with Session(engine) as session: db_hero = session.get(Hero, hero_id) diff --git a/docs_src/tutorial/fastapi/update/tutorial001_py310.py b/docs_src/tutorial/fastapi/update/tutorial001_py310.py index 02bec2e0db..a98ee68fbb 100644 --- a/docs_src/tutorial/fastapi/update/tutorial001_py310.py +++ b/docs_src/tutorial/fastapi/update/tutorial001_py310.py @@ -16,7 +16,7 @@ class HeroCreate(HeroBase): pass -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -45,7 +45,7 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(hero: HeroCreate): with Session(engine) as session: db_hero = Hero.model_validate(hero) @@ -55,14 +55,14 @@ def create_hero(hero: HeroCreate): return db_hero -@app.get("/heroes/", response_model=list[HeroRead]) +@app.get("/heroes/", response_model=list[HeroPublic]) def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): with Session(engine) as session: heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() return heroes -@app.get("/heroes/{hero_id}", response_model=HeroRead) +@app.get("/heroes/{hero_id}", response_model=HeroPublic) def read_hero(hero_id: int): with Session(engine) as session: hero = session.get(Hero, hero_id) @@ -71,7 +71,7 @@ def read_hero(hero_id: int): return hero -@app.patch("/heroes/{hero_id}", response_model=HeroRead) +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) def update_hero(hero_id: int, hero: HeroUpdate): with Session(engine) as session: db_hero = session.get(Hero, hero_id) diff --git a/docs_src/tutorial/fastapi/update/tutorial001_py39.py b/docs_src/tutorial/fastapi/update/tutorial001_py39.py index 241d205c05..b6d62bf81c 100644 --- a/docs_src/tutorial/fastapi/update/tutorial001_py39.py +++ b/docs_src/tutorial/fastapi/update/tutorial001_py39.py @@ -18,7 +18,7 @@ class HeroCreate(HeroBase): pass -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -47,7 +47,7 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(hero: HeroCreate): with Session(engine) as session: db_hero = Hero.model_validate(hero) @@ -57,14 +57,14 @@ def create_hero(hero: HeroCreate): return db_hero -@app.get("/heroes/", response_model=list[HeroRead]) +@app.get("/heroes/", response_model=list[HeroPublic]) def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): with Session(engine) as session: heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() return heroes -@app.get("/heroes/{hero_id}", response_model=HeroRead) +@app.get("/heroes/{hero_id}", response_model=HeroPublic) def read_hero(hero_id: int): with Session(engine) as session: hero = session.get(Hero, hero_id) @@ -73,7 +73,7 @@ def read_hero(hero_id: int): return hero -@app.patch("/heroes/{hero_id}", response_model=HeroRead) +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) def update_hero(hero_id: int, hero: HeroUpdate): with Session(engine) as session: db_hero = session.get(Hero, hero_id) diff --git a/docs_src/tutorial/fastapi/update/tutorial002.py b/docs_src/tutorial/fastapi/update/tutorial002.py index 1333654bcd..c8838aeffb 100644 --- a/docs_src/tutorial/fastapi/update/tutorial002.py +++ b/docs_src/tutorial/fastapi/update/tutorial002.py @@ -19,7 +19,7 @@ class HeroCreate(HeroBase): password: str -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -54,7 +54,7 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(hero: HeroCreate): hashed_password = hash_password(hero.password) with Session(engine) as session: @@ -66,14 +66,14 @@ def create_hero(hero: HeroCreate): return db_hero -@app.get("/heroes/", response_model=List[HeroRead]) +@app.get("/heroes/", response_model=List[HeroPublic]) def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): with Session(engine) as session: heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() return heroes -@app.get("/heroes/{hero_id}", response_model=HeroRead) +@app.get("/heroes/{hero_id}", response_model=HeroPublic) def read_hero(hero_id: int): with Session(engine) as session: hero = session.get(Hero, hero_id) @@ -82,7 +82,7 @@ def read_hero(hero_id: int): return hero -@app.patch("/heroes/{hero_id}", response_model=HeroRead) +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) def update_hero(hero_id: int, hero: HeroUpdate): with Session(engine) as session: db_hero = session.get(Hero, hero_id) diff --git a/docs_src/tutorial/fastapi/update/tutorial002_py310.py b/docs_src/tutorial/fastapi/update/tutorial002_py310.py index 6be274ef75..d250fecf3a 100644 --- a/docs_src/tutorial/fastapi/update/tutorial002_py310.py +++ b/docs_src/tutorial/fastapi/update/tutorial002_py310.py @@ -17,7 +17,7 @@ class HeroCreate(HeroBase): password: str -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -52,7 +52,7 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(hero: HeroCreate): hashed_password = hash_password(hero.password) with Session(engine) as session: @@ -64,14 +64,14 @@ def create_hero(hero: HeroCreate): return db_hero -@app.get("/heroes/", response_model=list[HeroRead]) +@app.get("/heroes/", response_model=list[HeroPublic]) def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): with Session(engine) as session: heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() return heroes -@app.get("/heroes/{hero_id}", response_model=HeroRead) +@app.get("/heroes/{hero_id}", response_model=HeroPublic) def read_hero(hero_id: int): with Session(engine) as session: hero = session.get(Hero, hero_id) @@ -80,7 +80,7 @@ def read_hero(hero_id: int): return hero -@app.patch("/heroes/{hero_id}", response_model=HeroRead) +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) def update_hero(hero_id: int, hero: HeroUpdate): with Session(engine) as session: db_hero = session.get(Hero, hero_id) diff --git a/docs_src/tutorial/fastapi/update/tutorial002_py39.py b/docs_src/tutorial/fastapi/update/tutorial002_py39.py index 19d30ea6fb..14ad1b4826 100644 --- a/docs_src/tutorial/fastapi/update/tutorial002_py39.py +++ b/docs_src/tutorial/fastapi/update/tutorial002_py39.py @@ -19,7 +19,7 @@ class HeroCreate(HeroBase): password: str -class HeroRead(HeroBase): +class HeroPublic(HeroBase): id: int @@ -54,7 +54,7 @@ def on_startup(): create_db_and_tables() -@app.post("/heroes/", response_model=HeroRead) +@app.post("/heroes/", response_model=HeroPublic) def create_hero(hero: HeroCreate): hashed_password = hash_password(hero.password) with Session(engine) as session: @@ -66,14 +66,14 @@ def create_hero(hero: HeroCreate): return db_hero -@app.get("/heroes/", response_model=list[HeroRead]) +@app.get("/heroes/", response_model=list[HeroPublic]) def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): with Session(engine) as session: heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() return heroes -@app.get("/heroes/{hero_id}", response_model=HeroRead) +@app.get("/heroes/{hero_id}", response_model=HeroPublic) def read_hero(hero_id: int): with Session(engine) as session: hero = session.get(Hero, hero_id) @@ -82,7 +82,7 @@ def read_hero(hero_id: int): return hero -@app.patch("/heroes/{hero_id}", response_model=HeroRead) +@app.patch("/heroes/{hero_id}", response_model=HeroPublic) def update_hero(hero_id: int, hero: HeroUpdate): with Session(engine) as session: db_hero = session.get(Hero, hero_id) diff --git a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py index 706cc8aed7..f293199b40 100644 --- a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py @@ -99,7 +99,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Heroes Heroes Get", "type": "array", "items": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" }, } } @@ -136,7 +136,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -172,7 +172,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -244,7 +244,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -297,8 +297,8 @@ def test_tutorial(clear_sqlmodel): ), }, }, - "HeroRead": { - "title": "HeroRead", + "HeroPublic": { + "title": "HeroPublic", "required": ["name", "secret_name", "id"], "type": "object", "properties": { diff --git a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py310.py index 46c8c42dd3..2757c878bc 100644 --- a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py310.py @@ -102,7 +102,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Heroes Heroes Get", "type": "array", "items": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" }, } } @@ -139,7 +139,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -175,7 +175,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -247,7 +247,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -300,8 +300,8 @@ def test_tutorial(clear_sqlmodel): ), }, }, - "HeroRead": { - "title": "HeroRead", + "HeroPublic": { + "title": "HeroPublic", "required": ["name", "secret_name", "id"], "type": "object", "properties": { diff --git a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py39.py index e2874c1095..3299086bd0 100644 --- a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py39.py @@ -102,7 +102,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Heroes Heroes Get", "type": "array", "items": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" }, } } @@ -139,7 +139,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -175,7 +175,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -247,7 +247,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -300,8 +300,8 @@ def test_tutorial(clear_sqlmodel): ), }, }, - "HeroRead": { - "title": "HeroRead", + "HeroPublic": { + "title": "HeroPublic", "required": ["name", "secret_name", "id"], "type": "object", "properties": { diff --git a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py index d177c80c4c..4047539f0a 100644 --- a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py @@ -104,7 +104,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Heroes Heroes Get", "type": "array", "items": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" }, } } @@ -141,7 +141,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -177,7 +177,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -230,8 +230,8 @@ def test_tutorial(clear_sqlmodel): ), }, }, - "HeroRead": { - "title": "HeroRead", + "HeroPublic": { + "title": "HeroPublic", "required": ["name", "secret_name", "id"], "type": "object", "properties": { diff --git a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py310.py index 03086996ca..480b92a121 100644 --- a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py310.py @@ -107,7 +107,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Heroes Heroes Get", "type": "array", "items": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" }, } } @@ -144,7 +144,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -180,7 +180,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -233,8 +233,8 @@ def test_tutorial(clear_sqlmodel): ), }, }, - "HeroRead": { - "title": "HeroRead", + "HeroPublic": { + "title": "HeroPublic", "required": ["name", "secret_name", "id"], "type": "object", "properties": { diff --git a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py39.py index f7e42e4e20..0a9d5c9ef0 100644 --- a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py39.py @@ -107,7 +107,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Heroes Heroes Get", "type": "array", "items": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" }, } } @@ -144,7 +144,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -180,7 +180,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -233,8 +233,8 @@ def test_tutorial(clear_sqlmodel): ), }, }, - "HeroRead": { - "title": "HeroRead", + "HeroPublic": { + "title": "HeroPublic", "required": ["name", "secret_name", "id"], "type": "object", "properties": { diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py index 2ebfc0c0d0..276a021c54 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py @@ -74,7 +74,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Heroes Heroes Get", "type": "array", "items": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" }, } } @@ -101,7 +101,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -154,8 +154,8 @@ def test_tutorial(clear_sqlmodel): ), }, }, - "HeroRead": { - "title": "HeroRead", + "HeroPublic": { + "title": "HeroPublic", "required": ["id", "name", "secret_name"], "type": "object", "properties": { diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py310.py index c17e482921..b6f082a0f8 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py310.py @@ -76,7 +76,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Heroes Heroes Get", "type": "array", "items": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" }, } } @@ -103,7 +103,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -156,8 +156,8 @@ def test_tutorial(clear_sqlmodel): ), }, }, - "HeroRead": { - "title": "HeroRead", + "HeroPublic": { + "title": "HeroPublic", "required": ["id", "name", "secret_name"], "type": "object", "properties": { diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py39.py index 258b3a4e54..82365ced61 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py39.py @@ -77,7 +77,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Heroes Heroes Get", "type": "array", "items": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" }, } } @@ -104,7 +104,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -157,8 +157,8 @@ def test_tutorial(clear_sqlmodel): ), }, }, - "HeroRead": { - "title": "HeroRead", + "HeroPublic": { + "title": "HeroPublic", "required": ["id", "name", "secret_name"], "type": "object", "properties": { diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py index 47f2e64155..8327c6d566 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py @@ -74,7 +74,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Heroes Heroes Get", "type": "array", "items": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" }, } } @@ -101,7 +101,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -154,8 +154,8 @@ def test_tutorial(clear_sqlmodel): ), }, }, - "HeroRead": { - "title": "HeroRead", + "HeroPublic": { + "title": "HeroPublic", "required": ["name", "secret_name", "id"], "type": "object", "properties": { diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py310.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py310.py index c09b15bd53..30edc4dea3 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py310.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py310.py @@ -77,7 +77,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Heroes Heroes Get", "type": "array", "items": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" }, } } @@ -104,7 +104,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -157,8 +157,8 @@ def test_tutorial(clear_sqlmodel): ), }, }, - "HeroRead": { - "title": "HeroRead", + "HeroPublic": { + "title": "HeroPublic", "required": ["name", "secret_name", "id"], "type": "object", "properties": { diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py39.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py39.py index 8ad0f271e1..2b86d3facc 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py39.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py39.py @@ -77,7 +77,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Heroes Heroes Get", "type": "array", "items": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" }, } } @@ -104,7 +104,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -157,8 +157,8 @@ def test_tutorial(clear_sqlmodel): ), }, }, - "HeroRead": { - "title": "HeroRead", + "HeroPublic": { + "title": "HeroPublic", "required": ["name", "secret_name", "id"], "type": "object", "properties": { diff --git a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py index 62fbb25a9c..9b1d527565 100644 --- a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py @@ -59,7 +59,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Heroes Heroes Get", "type": "array", "items": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" }, } } @@ -86,7 +86,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -122,7 +122,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -175,8 +175,8 @@ def test_tutorial(clear_sqlmodel): ), }, }, - "HeroRead": { - "title": "HeroRead", + "HeroPublic": { + "title": "HeroPublic", "required": ["name", "secret_name", "id"], "type": "object", "properties": { diff --git a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py310.py index 913d098882..f18b0d65cf 100644 --- a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py310.py @@ -62,7 +62,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Heroes Heroes Get", "type": "array", "items": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" }, } } @@ -89,7 +89,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -125,7 +125,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -178,8 +178,8 @@ def test_tutorial(clear_sqlmodel): ), }, }, - "HeroRead": { - "title": "HeroRead", + "HeroPublic": { + "title": "HeroPublic", "required": ["name", "secret_name", "id"], "type": "object", "properties": { diff --git a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py39.py index 9bedf5c62d..4423d1a713 100644 --- a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py39.py @@ -62,7 +62,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Heroes Heroes Get", "type": "array", "items": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" }, } } @@ -89,7 +89,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -125,7 +125,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -178,8 +178,8 @@ def test_tutorial(clear_sqlmodel): ), }, }, - "HeroRead": { - "title": "HeroRead", + "HeroPublic": { + "title": "HeroPublic", "required": ["name", "secret_name", "id"], "type": "object", "properties": { diff --git a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py index b301697dbf..4b4f47b762 100644 --- a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py @@ -147,7 +147,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Heroes Heroes Get", "type": "array", "items": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" }, } } @@ -184,7 +184,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -220,7 +220,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroReadWithTeam" + "$ref": "#/components/schemas/HeroPublicWithTeam" } } }, @@ -292,7 +292,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -346,7 +346,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Teams Teams Get", "type": "array", "items": { - "$ref": "#/components/schemas/TeamRead" + "$ref": "#/components/schemas/TeamPublic" }, } } @@ -383,7 +383,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TeamRead" + "$ref": "#/components/schemas/TeamPublic" } } }, @@ -419,7 +419,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TeamReadWithHeroes" + "$ref": "#/components/schemas/TeamPublicWithHeroes" } } }, @@ -491,7 +491,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TeamRead" + "$ref": "#/components/schemas/TeamPublic" } } }, @@ -554,8 +554,8 @@ def test_tutorial(clear_sqlmodel): ), }, }, - "HeroRead": { - "title": "HeroRead", + "HeroPublic": { + "title": "HeroPublic", "required": ["name", "secret_name", "id"], "type": "object", "properties": { @@ -584,8 +584,8 @@ def test_tutorial(clear_sqlmodel): "id": {"title": "Id", "type": "integer"}, }, }, - "HeroReadWithTeam": { - "title": "HeroReadWithTeam", + "HeroPublicWithTeam": { + "title": "HeroPublicWithTeam", "required": ["name", "secret_name", "id"], "type": "object", "properties": { @@ -615,14 +615,14 @@ def test_tutorial(clear_sqlmodel): "team": IsDict( { "anyOf": [ - {"$ref": "#/components/schemas/TeamRead"}, + {"$ref": "#/components/schemas/TeamPublic"}, {"type": "null"}, ] } ) | IsDict( # TODO: remove when deprecating Pydantic v1 - {"$ref": "#/components/schemas/TeamRead"} + {"$ref": "#/components/schemas/TeamPublic"} ), }, }, @@ -681,8 +681,8 @@ def test_tutorial(clear_sqlmodel): "headquarters": {"title": "Headquarters", "type": "string"}, }, }, - "TeamRead": { - "title": "TeamRead", + "TeamPublic": { + "title": "TeamPublic", "required": ["name", "headquarters", "id"], "type": "object", "properties": { @@ -691,8 +691,8 @@ def test_tutorial(clear_sqlmodel): "id": {"title": "Id", "type": "integer"}, }, }, - "TeamReadWithHeroes": { - "title": "TeamReadWithHeroes", + "TeamPublicWithHeroes": { + "title": "TeamPublicWithHeroes", "required": ["name", "headquarters", "id"], "type": "object", "properties": { @@ -702,7 +702,7 @@ def test_tutorial(clear_sqlmodel): "heroes": { "title": "Heroes", "type": "array", - "items": {"$ref": "#/components/schemas/HeroRead"}, + "items": {"$ref": "#/components/schemas/HeroPublic"}, "default": [], }, }, diff --git a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py310.py index 4d310a87e5..dcb78f597d 100644 --- a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py310.py @@ -150,7 +150,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Heroes Heroes Get", "type": "array", "items": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" }, } } @@ -187,7 +187,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -223,7 +223,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroReadWithTeam" + "$ref": "#/components/schemas/HeroPublicWithTeam" } } }, @@ -295,7 +295,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -349,7 +349,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Teams Teams Get", "type": "array", "items": { - "$ref": "#/components/schemas/TeamRead" + "$ref": "#/components/schemas/TeamPublic" }, } } @@ -386,7 +386,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TeamRead" + "$ref": "#/components/schemas/TeamPublic" } } }, @@ -422,7 +422,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TeamReadWithHeroes" + "$ref": "#/components/schemas/TeamPublicWithHeroes" } } }, @@ -494,7 +494,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TeamRead" + "$ref": "#/components/schemas/TeamPublic" } } }, @@ -557,8 +557,8 @@ def test_tutorial(clear_sqlmodel): ), }, }, - "HeroRead": { - "title": "HeroRead", + "HeroPublic": { + "title": "HeroPublic", "required": ["name", "secret_name", "id"], "type": "object", "properties": { @@ -587,8 +587,8 @@ def test_tutorial(clear_sqlmodel): "id": {"title": "Id", "type": "integer"}, }, }, - "HeroReadWithTeam": { - "title": "HeroReadWithTeam", + "HeroPublicWithTeam": { + "title": "HeroPublicWithTeam", "required": ["name", "secret_name", "id"], "type": "object", "properties": { @@ -618,14 +618,14 @@ def test_tutorial(clear_sqlmodel): "team": IsDict( { "anyOf": [ - {"$ref": "#/components/schemas/TeamRead"}, + {"$ref": "#/components/schemas/TeamPublic"}, {"type": "null"}, ] } ) | IsDict( # TODO: remove when deprecating Pydantic v1 - {"$ref": "#/components/schemas/TeamRead"} + {"$ref": "#/components/schemas/TeamPublic"} ), }, }, @@ -684,8 +684,8 @@ def test_tutorial(clear_sqlmodel): "headquarters": {"title": "Headquarters", "type": "string"}, }, }, - "TeamRead": { - "title": "TeamRead", + "TeamPublic": { + "title": "TeamPublic", "required": ["name", "headquarters", "id"], "type": "object", "properties": { @@ -694,8 +694,8 @@ def test_tutorial(clear_sqlmodel): "id": {"title": "Id", "type": "integer"}, }, }, - "TeamReadWithHeroes": { - "title": "TeamReadWithHeroes", + "TeamPublicWithHeroes": { + "title": "TeamPublicWithHeroes", "required": ["name", "headquarters", "id"], "type": "object", "properties": { @@ -705,7 +705,7 @@ def test_tutorial(clear_sqlmodel): "heroes": { "title": "Heroes", "type": "array", - "items": {"$ref": "#/components/schemas/HeroRead"}, + "items": {"$ref": "#/components/schemas/HeroPublic"}, "default": [], }, }, diff --git a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py39.py index 0603739c41..5ef7338d44 100644 --- a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py39.py @@ -150,7 +150,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Heroes Heroes Get", "type": "array", "items": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" }, } } @@ -187,7 +187,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -223,7 +223,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroReadWithTeam" + "$ref": "#/components/schemas/HeroPublicWithTeam" } } }, @@ -295,7 +295,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -349,7 +349,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Teams Teams Get", "type": "array", "items": { - "$ref": "#/components/schemas/TeamRead" + "$ref": "#/components/schemas/TeamPublic" }, } } @@ -386,7 +386,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TeamRead" + "$ref": "#/components/schemas/TeamPublic" } } }, @@ -422,7 +422,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TeamReadWithHeroes" + "$ref": "#/components/schemas/TeamPublicWithHeroes" } } }, @@ -494,7 +494,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TeamRead" + "$ref": "#/components/schemas/TeamPublic" } } }, @@ -557,8 +557,8 @@ def test_tutorial(clear_sqlmodel): ), }, }, - "HeroRead": { - "title": "HeroRead", + "HeroPublic": { + "title": "HeroPublic", "required": ["name", "secret_name", "id"], "type": "object", "properties": { @@ -587,8 +587,8 @@ def test_tutorial(clear_sqlmodel): "id": {"title": "Id", "type": "integer"}, }, }, - "HeroReadWithTeam": { - "title": "HeroReadWithTeam", + "HeroPublicWithTeam": { + "title": "HeroPublicWithTeam", "required": ["name", "secret_name", "id"], "type": "object", "properties": { @@ -618,14 +618,14 @@ def test_tutorial(clear_sqlmodel): "team": IsDict( { "anyOf": [ - {"$ref": "#/components/schemas/TeamRead"}, + {"$ref": "#/components/schemas/TeamPublic"}, {"type": "null"}, ] } ) | IsDict( # TODO: remove when deprecating Pydantic v1 - {"$ref": "#/components/schemas/TeamRead"} + {"$ref": "#/components/schemas/TeamPublic"} ), }, }, @@ -684,8 +684,8 @@ def test_tutorial(clear_sqlmodel): "headquarters": {"title": "Headquarters", "type": "string"}, }, }, - "TeamRead": { - "title": "TeamRead", + "TeamPublic": { + "title": "TeamPublic", "required": ["name", "headquarters", "id"], "type": "object", "properties": { @@ -694,8 +694,8 @@ def test_tutorial(clear_sqlmodel): "id": {"title": "Id", "type": "integer"}, }, }, - "TeamReadWithHeroes": { - "title": "TeamReadWithHeroes", + "TeamPublicWithHeroes": { + "title": "TeamPublicWithHeroes", "required": ["name", "headquarters", "id"], "type": "object", "properties": { @@ -705,7 +705,7 @@ def test_tutorial(clear_sqlmodel): "heroes": { "title": "Heroes", "type": "array", - "items": {"$ref": "#/components/schemas/HeroRead"}, + "items": {"$ref": "#/components/schemas/HeroPublic"}, "default": [], }, }, diff --git a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py index 441cc42b28..388cfa9b2b 100644 --- a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py @@ -99,7 +99,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Heroes Heroes Get", "type": "array", "items": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" }, } } @@ -136,7 +136,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -172,7 +172,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -244,7 +244,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -297,8 +297,8 @@ def test_tutorial(clear_sqlmodel): ), }, }, - "HeroRead": { - "title": "HeroRead", + "HeroPublic": { + "title": "HeroPublic", "required": ["name", "secret_name", "id"], "type": "object", "properties": { diff --git a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py310.py index 7c427a1c67..65bab47735 100644 --- a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py310.py @@ -104,7 +104,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Heroes Heroes Get", "type": "array", "items": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" }, } } @@ -141,7 +141,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -177,7 +177,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -249,7 +249,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -302,8 +302,8 @@ def test_tutorial(clear_sqlmodel): ), }, }, - "HeroRead": { - "title": "HeroRead", + "HeroPublic": { + "title": "HeroPublic", "required": ["name", "secret_name", "id"], "type": "object", "properties": { diff --git a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py39.py index ea63f52c41..cdab85df17 100644 --- a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py39.py @@ -104,7 +104,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Heroes Heroes Get", "type": "array", "items": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" }, } } @@ -141,7 +141,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -177,7 +177,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -249,7 +249,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -302,8 +302,8 @@ def test_tutorial(clear_sqlmodel): ), }, }, - "HeroRead": { - "title": "HeroRead", + "HeroPublic": { + "title": "HeroPublic", "required": ["name", "secret_name", "id"], "type": "object", "properties": { diff --git a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py index a532625d42..25daadf74b 100644 --- a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py @@ -134,7 +134,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Heroes Heroes Get", "type": "array", "items": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" }, } } @@ -171,7 +171,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -207,7 +207,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -279,7 +279,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -333,7 +333,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Teams Teams Get", "type": "array", "items": { - "$ref": "#/components/schemas/TeamRead" + "$ref": "#/components/schemas/TeamPublic" }, } } @@ -370,7 +370,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TeamRead" + "$ref": "#/components/schemas/TeamPublic" } } }, @@ -406,7 +406,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TeamRead" + "$ref": "#/components/schemas/TeamPublic" } } }, @@ -478,7 +478,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TeamRead" + "$ref": "#/components/schemas/TeamPublic" } } }, @@ -541,8 +541,8 @@ def test_tutorial(clear_sqlmodel): ), }, }, - "HeroRead": { - "title": "HeroRead", + "HeroPublic": { + "title": "HeroPublic", "required": ["name", "secret_name", "id"], "type": "object", "properties": { @@ -626,8 +626,8 @@ def test_tutorial(clear_sqlmodel): "headquarters": {"title": "Headquarters", "type": "string"}, }, }, - "TeamRead": { - "title": "TeamRead", + "TeamPublic": { + "title": "TeamPublic", "required": ["name", "headquarters", "id"], "type": "object", "properties": { diff --git a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py310.py index 33029f6b60..63f8a1d70b 100644 --- a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py310.py @@ -137,7 +137,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Heroes Heroes Get", "type": "array", "items": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" }, } } @@ -174,7 +174,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -210,7 +210,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -282,7 +282,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -336,7 +336,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Teams Teams Get", "type": "array", "items": { - "$ref": "#/components/schemas/TeamRead" + "$ref": "#/components/schemas/TeamPublic" }, } } @@ -373,7 +373,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TeamRead" + "$ref": "#/components/schemas/TeamPublic" } } }, @@ -409,7 +409,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TeamRead" + "$ref": "#/components/schemas/TeamPublic" } } }, @@ -481,7 +481,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TeamRead" + "$ref": "#/components/schemas/TeamPublic" } } }, @@ -544,8 +544,8 @@ def test_tutorial(clear_sqlmodel): ), }, }, - "HeroRead": { - "title": "HeroRead", + "HeroPublic": { + "title": "HeroPublic", "required": ["name", "secret_name", "id"], "type": "object", "properties": { @@ -629,8 +629,8 @@ def test_tutorial(clear_sqlmodel): "headquarters": {"title": "Headquarters", "type": "string"}, }, }, - "TeamRead": { - "title": "TeamRead", + "TeamPublic": { + "title": "TeamPublic", "required": ["name", "headquarters", "id"], "type": "object", "properties": { diff --git a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py39.py index 66705e17cc..30b68e0ed9 100644 --- a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py39.py @@ -137,7 +137,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Heroes Heroes Get", "type": "array", "items": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" }, } } @@ -174,7 +174,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -210,7 +210,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -282,7 +282,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -336,7 +336,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Teams Teams Get", "type": "array", "items": { - "$ref": "#/components/schemas/TeamRead" + "$ref": "#/components/schemas/TeamPublic" }, } } @@ -373,7 +373,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TeamRead" + "$ref": "#/components/schemas/TeamPublic" } } }, @@ -409,7 +409,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TeamRead" + "$ref": "#/components/schemas/TeamPublic" } } }, @@ -481,7 +481,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TeamRead" + "$ref": "#/components/schemas/TeamPublic" } } }, @@ -544,8 +544,8 @@ def test_tutorial(clear_sqlmodel): ), }, }, - "HeroRead": { - "title": "HeroRead", + "HeroPublic": { + "title": "HeroPublic", "required": ["name", "secret_name", "id"], "type": "object", "properties": { @@ -629,8 +629,8 @@ def test_tutorial(clear_sqlmodel): "headquarters": {"title": "Headquarters", "type": "string"}, }, }, - "TeamRead": { - "title": "TeamRead", + "TeamPublic": { + "title": "TeamPublic", "required": ["name", "headquarters", "id"], "type": "object", "properties": { diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py index 973ab2db04..0856b24676 100644 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py @@ -106,7 +106,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Heroes Heroes Get", "type": "array", "items": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" }, } } @@ -143,7 +143,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -179,7 +179,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -223,7 +223,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -276,8 +276,8 @@ def test_tutorial(clear_sqlmodel): ), }, }, - "HeroRead": { - "title": "HeroRead", + "HeroPublic": { + "title": "HeroPublic", "required": ["name", "secret_name", "id"], "type": "object", "properties": { diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py310.py index 090af8c60f..d79b2ecea3 100644 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py310.py @@ -109,7 +109,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Heroes Heroes Get", "type": "array", "items": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" }, } } @@ -146,7 +146,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -182,7 +182,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -226,7 +226,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -279,8 +279,8 @@ def test_tutorial(clear_sqlmodel): ), }, }, - "HeroRead": { - "title": "HeroRead", + "HeroPublic": { + "title": "HeroPublic", "required": ["name", "secret_name", "id"], "type": "object", "properties": { diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py39.py index 22dfb8f268..1be81dec2e 100644 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py39.py @@ -109,7 +109,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Heroes Heroes Get", "type": "array", "items": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" }, } } @@ -146,7 +146,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -182,7 +182,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -226,7 +226,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -279,8 +279,8 @@ def test_tutorial(clear_sqlmodel): ), }, }, - "HeroRead": { - "title": "HeroRead", + "HeroPublic": { + "title": "HeroPublic", "required": ["name", "secret_name", "id"], "type": "object", "properties": { diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial002.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial002.py index 21ca74e53f..32e343ed61 100644 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial002.py +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial002.py @@ -169,7 +169,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Heroes Heroes Get", "type": "array", "items": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" }, } } @@ -206,7 +206,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -242,7 +242,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -286,7 +286,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -340,8 +340,8 @@ def test_tutorial(clear_sqlmodel): "password": {"type": "string", "title": "Password"}, }, }, - "HeroRead": { - "title": "HeroRead", + "HeroPublic": { + "title": "HeroPublic", "required": ["name", "secret_name", "id"], "type": "object", "properties": { diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py310.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py310.py index 6feb1ec54e..b05f5b2133 100644 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py310.py +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py310.py @@ -172,7 +172,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Heroes Heroes Get", "type": "array", "items": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" }, } } @@ -209,7 +209,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -245,7 +245,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -289,7 +289,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -343,8 +343,8 @@ def test_tutorial(clear_sqlmodel): "password": {"type": "string", "title": "Password"}, }, }, - "HeroRead": { - "title": "HeroRead", + "HeroPublic": { + "title": "HeroPublic", "required": ["name", "secret_name", "id"], "type": "object", "properties": { diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py39.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py39.py index 13d70dd3e8..807e33421a 100644 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py39.py +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py39.py @@ -172,7 +172,7 @@ def test_tutorial(clear_sqlmodel): "title": "Response Read Heroes Heroes Get", "type": "array", "items": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" }, } } @@ -209,7 +209,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -245,7 +245,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -289,7 +289,7 @@ def test_tutorial(clear_sqlmodel): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HeroRead" + "$ref": "#/components/schemas/HeroPublic" } } }, @@ -343,8 +343,8 @@ def test_tutorial(clear_sqlmodel): "password": {"type": "string", "title": "Password"}, }, }, - "HeroRead": { - "title": "HeroRead", + "HeroPublic": { + "title": "HeroPublic", "required": ["name", "secret_name", "id"], "type": "object", "properties": { From 6151f23e15703223128d641081b63d49a658d524 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 8 Apr 2024 23:08:14 +0000 Subject: [PATCH 350/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 75cec39e7b..eba16d13a8 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Docs +* 📝 Update ModelRead to ModelPublic documentation and examples. PR [#885](https://github.com/tiangolo/sqlmodel/pull/885) by [@estebanx64](https://github.com/estebanx64). * ✨ Add source examples for Python 3.10 and 3.9 with updated syntax. PR [#842](https://github.com/tiangolo/sqlmodel/pull/842) by [@tiangolo](https://github.com/tiangolo) and [@estebanx64](https://github.com/estebanx64). ### Internal From 2454694de330f2e986f981397d7cef90393d573e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 29 Apr 2024 15:11:02 -0700 Subject: [PATCH 351/906] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20types?= =?UTF-8?q?=20to=20properly=20support=20Pydantic=202.7=20(#913)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/_compat.py | 4 +++- sqlmodel/main.py | 20 +++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index 072d2b0f58..72ec8330fd 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -18,11 +18,13 @@ Union, ) -from pydantic import VERSION as PYDANTIC_VERSION +from pydantic import VERSION as P_VERSION from pydantic import BaseModel from pydantic.fields import FieldInfo from typing_extensions import get_args, get_origin +# Reassign variable to make it reexported for mypy +PYDANTIC_VERSION = P_VERSION IS_PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.") diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 9e8330d69d..a16428b192 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -6,6 +6,7 @@ from enum import Enum from pathlib import Path from typing import ( + TYPE_CHECKING, AbstractSet, Any, Callable, @@ -55,6 +56,7 @@ from ._compat import ( # type: ignore[attr-defined] IS_PYDANTIC_V2, + PYDANTIC_VERSION, BaseConfig, ModelField, ModelMetaclass, @@ -80,6 +82,12 @@ ) from .sql.sqltypes import GUID, AutoString +if TYPE_CHECKING: + from pydantic._internal._model_construction import ModelMetaclass as ModelMetaclass + from pydantic._internal._repr import Representation as Representation + from pydantic_core import PydanticUndefined as Undefined + from pydantic_core import PydanticUndefinedType as UndefinedType + _T = TypeVar("_T") NoArgAnyCallable = Callable[[], Any] IncEx = Union[Set[int], Set[str], Dict[int, Any], Dict[str, Any], None] @@ -764,13 +772,22 @@ def model_dump( mode: Union[Literal["json", "python"], str] = "python", include: IncEx = None, exclude: IncEx = None, + context: Union[Dict[str, Any], None] = None, by_alias: bool = False, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, round_trip: bool = False, - warnings: bool = True, + warnings: Union[bool, Literal["none", "warn", "error"]] = True, + serialize_as_any: bool = False, ) -> Dict[str, Any]: + if PYDANTIC_VERSION >= "2.7.0": + extra_kwargs: Dict[str, Any] = { + "context": context, + "serialize_as_any": serialize_as_any, + } + else: + extra_kwargs = {} if IS_PYDANTIC_V2: return super().model_dump( mode=mode, @@ -782,6 +799,7 @@ def model_dump( exclude_none=exclude_none, round_trip=round_trip, warnings=warnings, + **extra_kwargs, ) else: return super().dict( From f67867f9741aecde2f70a09d08baf7197003dca2 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 29 Apr 2024 22:11:20 +0000 Subject: [PATCH 352/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index eba16d13a8..1fef70b248 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Refactors + +* ♻️ Refactor types to properly support Pydantic 2.7. PR [#913](https://github.com/tiangolo/sqlmodel/pull/913) by [@tiangolo](https://github.com/tiangolo). + ### Docs * 📝 Update ModelRead to ModelPublic documentation and examples. PR [#885](https://github.com/tiangolo/sqlmodel/pull/885) by [@estebanx64](https://github.com/estebanx64). From 0b4989d0b2bdc15fdf7499f62cc63198a582a9fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 29 Apr 2024 15:58:15 -0700 Subject: [PATCH 353/906] =?UTF-8?q?=F0=9F=94=A7=20Migrate=20from=20Poetry?= =?UTF-8?q?=20to=20PDM=20for=20the=20internal=20build=20config=20(#912)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 20 +++----- .github/workflows/publish.yml | 42 ++++++---------- .github/workflows/test-redistribute.yml | 50 +++++++++++++++++++ .github/workflows/test.yml | 16 ++---- pyproject.toml | 65 ++++++++++++------------- requirements-docs-tests.txt | 2 + requirements-docs.txt | 18 +++++++ requirements-tests.txt | 12 +++++ requirements.txt | 6 +++ sqlmodel/__init__.py | 2 +- 10 files changed, 145 insertions(+), 88 deletions(-) create mode 100644 .github/workflows/test-redistribute.yml create mode 100644 requirements-docs-tests.txt create mode 100644 requirements-docs.txt create mode 100644 requirements-tests.txt create mode 100644 requirements.txt diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index e7de374652..4871f7bb47 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -27,6 +27,7 @@ jobs: - README.md - docs/** - docs_src/** + - requirements-docs.txt - pyproject.toml - mkdocs.yml - mkdocs.insiders.yml @@ -52,21 +53,16 @@ jobs: id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-v01 - - name: Install Poetry + key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-docs.txt') }}-v01 + - name: Install docs extras if: steps.cache.outputs.cache-hit != 'true' - run: | - python -m pip install --upgrade pip - python -m pip install "poetry" - python -m poetry self add poetry-version-plugin - - name: Configure poetry - run: python -m poetry config virtualenvs.create false - - name: Install Dependencies - if: steps.cache.outputs.cache-hit != 'true' - run: python -m poetry install + run: pip install -r requirements-docs.txt - name: Install Material for MkDocs Insiders if: ( github.event_name != 'pull_request' || github.secret_source == 'Actions' ) && steps.cache.outputs.cache-hit != 'true' - run: python -m poetry run pip install git+https://${{ secrets.SQLMODEL_MKDOCS_MATERIAL_INSIDERS }}@github.com/squidfunk/mkdocs-material-insiders.git + run: | + pip install git+https://${{ secrets.SQLMODEL_MKDOCS_MATERIAL_INSIDERS }}@github.com/squidfunk/mkdocs-material-insiders.git + pip install git+https://${{ secrets.SQLMODEL_MKDOCS_MATERIAL_INSIDERS }}@github.com/pawamoy-insiders/griffe-typing-deprecated.git + pip install git+https://${{ secrets.SQLMODEL_MKDOCS_MATERIAL_INSIDERS }}@github.com/pawamoy-insiders/mkdocstrings-python.git - uses: actions/cache@v3 with: key: mkdocs-cards-${{ github.ref }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 42a0eec3ce..911cd623a7 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -14,37 +14,23 @@ on: jobs: publish: runs-on: ubuntu-latest + strategy: + matrix: + package: + - sqlmodel + permissions: + id-token: write steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.7" - # Allow debugging with tmate - - name: Setup tmate session - uses: mxschmitt/action-tmate@v3 - if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} - with: - limit-access-to-actor: true - - uses: actions/cache@v3 - id: cache - with: - path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-root-v2 - - name: Install poetry - if: steps.cache.outputs.cache-hit != 'true' - run: | - python -m pip install --upgrade pip - python -m pip install "poetry" - python -m poetry self add poetry-version-plugin - - name: Configure poetry - run: python -m poetry config virtualenvs.create false - - name: Install Dependencies - if: steps.cache.outputs.cache-hit != 'true' - run: python -m poetry install - - name: Publish + python-version: "3.11" + - name: Install build dependencies + run: pip install build + - name: Build distribution env: - PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} - run: | - python -m poetry config pypi-token.pypi $PYPI_TOKEN - bash scripts/publish.sh + TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }} + run: python -m build + - name: Publish + uses: pypa/gh-action-pypi-publish@v1.8.11 diff --git a/.github/workflows/test-redistribute.yml b/.github/workflows/test-redistribute.yml new file mode 100644 index 0000000000..45b82414c4 --- /dev/null +++ b/.github/workflows/test-redistribute.yml @@ -0,0 +1,50 @@ +name: Test Redistribute + +on: + push: + branches: + - main + pull_request: + types: + - opened + - synchronize + +jobs: + test-redistribute: + runs-on: ubuntu-latest + strategy: + matrix: + package: + - sqlmodel + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + - name: Install build dependencies + run: pip install build + - name: Build source distribution + env: + TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }} + run: python -m build --sdist + - name: Decompress source distribution + run: | + cd dist + tar xvf sqlmodel*.tar.gz + - name: Install test dependencies + run: | + cd dist/sqlmodel*/ + pip install -r requirements-tests.txt + - name: Run source distribution tests + run: | + cd dist/sqlmodel*/ + bash scripts/test.sh + - name: Build wheel distribution + run: | + cd dist + pip wheel --no-deps sqlmodel*.tar.gz diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 990bf46de4..f0872ef623 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -51,18 +51,10 @@ jobs: id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-root-v2 - - name: Install poetry - if: steps.cache.outputs.cache-hit != 'true' - run: | - python -m pip install --upgrade pip - python -m pip install "poetry" - python -m poetry self add poetry-version-plugin - - name: Configure poetry - run: python -m poetry config virtualenvs.create false + key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }} - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' - run: python -m poetry install + run: pip install -r requirements-tests.txt - name: Install Pydantic v1 if: matrix.pydantic-version == 'pydantic-v1' run: pip install --upgrade "pydantic>=1.10.0,<2.0.0" @@ -72,10 +64,10 @@ jobs: - name: Lint # Do not run on Python 3.7 as mypy behaves differently if: matrix.python-version != '3.7' && matrix.pydantic-version == 'pydantic-v2' - run: python -m poetry run bash scripts/lint.sh + run: bash scripts/lint.sh - run: mkdir coverage - name: Test - run: python -m poetry run bash scripts/test.sh + run: bash scripts/test.sh env: COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }} CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }} diff --git a/pyproject.toml b/pyproject.toml index 9da631b985..17f72941b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,17 @@ -[tool.poetry] +[build-system] +requires = ["pdm-backend"] +build-backend = "pdm.backend" + +[project] name = "sqlmodel" -version = "0" +dynamic = ["version"] description = "SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness." -authors = ["Sebastián Ramírez "] readme = "README.md" -homepage = "https://github.com/tiangolo/sqlmodel" -documentation = "https://sqlmodel.tiangolo.com" -repository = "https://github.com/tiangolo/sqlmodel" -license = "MIT" -exclude = ["sqlmodel/sql/expression.py.jinja2"] +requires-python = ">=3.7" +authors = [ + { name = "Sebastián Ramírez", email = "tiangolo@gmail.com" }, +] + classifiers = [ "Development Status :: 4 - Beta", "Framework :: AsyncIO", @@ -31,36 +34,28 @@ classifiers = [ "Typing :: Typed", ] -[tool.poetry.dependencies] -python = "^3.7" -SQLAlchemy = ">=2.0.0,<2.1.0" -pydantic = ">=1.10.13,<3.0.0" +dependencies = [ + "SQLAlchemy >=2.0.0,<2.1.0", + "pydantic >=1.10.13,<3.0.0", +] -[tool.poetry.group.dev.dependencies] -pytest = "^7.0.1" -mypy = "1.4.1" -# Needed by the code generator using templates -black = ">=22.10,<24.0" -mkdocs-material = "9.2.7" -pillow = "^9.3.0" -cairosvg = "^2.5.2" -mdx-include = "^1.4.1" -coverage = {extras = ["toml"], version = ">=6.2,<8.0"} -fastapi = "^0.103.2" -ruff = "0.2.0" -# For FastAPI tests -httpx = "0.24.1" -# TODO: upgrade when deprecating Python 3.7 -dirty-equals = "^0.6.0" -typer-cli = "^0.0.13" -mkdocs-markdownextradata-plugin = ">=0.1.7,<0.3.0" +[project.urls] +Homepage = "https://github.com/tiangolo/sqlmodel" +Documentation = "https://sqlmodel.tiangolo.com" +Repository = "https://github.com/tiangolo/sqlmodel" -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +[tool.pdm] +version = { source = "file", path = "sqlmodel/__init__.py" } +distribution = true -[tool.poetry-version-plugin] -source = "init" +[tool.pdm.build] +source-includes = [ + "tests/", + "docs_src/", + "requirements*.txt", + "scripts/", + "sqlmodel/sql/expression.py.jinja2", + ] [tool.coverage.run] parallel = true diff --git a/requirements-docs-tests.txt b/requirements-docs-tests.txt new file mode 100644 index 0000000000..28f1ad1be9 --- /dev/null +++ b/requirements-docs-tests.txt @@ -0,0 +1,2 @@ +# For mkdocstrings and code generator using templates +black >=22.10,<24.0 diff --git a/requirements-docs.txt b/requirements-docs.txt new file mode 100644 index 0000000000..cacb5dc2a3 --- /dev/null +++ b/requirements-docs.txt @@ -0,0 +1,18 @@ +-e . +-r requirements-docs-tests.txt +mkdocs-material==9.4.7 +mdx-include >=1.4.1,<2.0.0 +mkdocs-markdownextradata-plugin >=0.1.7,<0.3.0 +mkdocs-redirects>=1.2.1,<1.3.0 +pyyaml >=5.3.1,<7.0.0 +# For Material for MkDocs, Chinese search +jieba==0.42.1 +# For image processing by Material for MkDocs +pillow==10.1.0 +# For image processing by Material for MkDocs +cairosvg==2.7.0 +mkdocstrings[python]==0.23.0 +griffe-typingdoc==0.2.2 +# For griffe, it formats with black +black==23.3.0 +typer == 0.12.3 diff --git a/requirements-tests.txt b/requirements-tests.txt new file mode 100644 index 0000000000..648f99b1c9 --- /dev/null +++ b/requirements-tests.txt @@ -0,0 +1,12 @@ +-e . +-r requirements-docs-tests.txt +pytest >=7.0.1,<8.0.0 +coverage[toml] >=6.2,<8.0 +mypy ==1.4.1 +ruff ==0.2.0 +# For FastAPI tests +fastapi >=0.103.2 +httpx ==0.24.1 +# TODO: upgrade when deprecating Python 3.7 +dirty-equals ==0.6.0 +jinja2 ==3.1.3 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000..1e21c5d2f3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +-e . + +-r requirements-tests.txt +-r requirements-docs.txt + +pre-commit >=2.17.0,<4.0.0 diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index 556bba1895..fac039e819 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.0.16" +__version__ = "0.0.17.dev2" # Re-export from SQLAlchemy from sqlalchemy.engine import create_engine as create_engine From 570cd9f10c6bed37b1a674ff7e99a0ee2b962c2d Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 29 Apr 2024 22:58:36 +0000 Subject: [PATCH 354/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 1fef70b248..b55bdcdc8e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -13,6 +13,7 @@ ### Internal +* 🔧 Migrate from Poetry to PDM for the internal build config. PR [#912](https://github.com/tiangolo/sqlmodel/pull/912) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update MkDocs, disable cards while I can upgrade to the latest MkDocs Material, that fixes an issue with social cards. PR [#888](https://github.com/tiangolo/sqlmodel/pull/888) by [@tiangolo](https://github.com/tiangolo). * 👷 Add cron to run test once a week on monday. PR [#869](https://github.com/tiangolo/sqlmodel/pull/869) by [@estebanx64](https://github.com/estebanx64). * ⬆️ Upgrade Ruff version and configs. PR [#859](https://github.com/tiangolo/sqlmodel/pull/859) by [@tiangolo](https://github.com/tiangolo). From 7023896d7c25bf99aade92e7e2c2a04db250b1ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 29 Apr 2024 16:24:50 -0700 Subject: [PATCH 355/906] =?UTF-8?q?=F0=9F=94=A8=20Update=20internal=20scri?= =?UTF-8?q?pts=20and=20remove=20unused=20ones=20(#914)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .flake8 | 5 ----- scripts/docs-live.sh | 7 ------- scripts/format.sh | 2 +- scripts/lint.sh | 2 +- scripts/publish.sh | 5 ----- scripts/test-files.sh | 7 ------- scripts/zip-docs.sh | 11 ----------- 7 files changed, 2 insertions(+), 37 deletions(-) delete mode 100644 .flake8 delete mode 100755 scripts/docs-live.sh delete mode 100755 scripts/publish.sh delete mode 100755 scripts/test-files.sh delete mode 100644 scripts/zip-docs.sh diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 47ef7c07fc..0000000000 --- a/.flake8 +++ /dev/null @@ -1,5 +0,0 @@ -[flake8] -max-line-length = 88 -select = C,E,F,W,B,B9 -ignore = E203, E501, W503 -exclude = __init__.py diff --git a/scripts/docs-live.sh b/scripts/docs-live.sh deleted file mode 100755 index b6071fdb97..0000000000 --- a/scripts/docs-live.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -set -e - -export DYLD_FALLBACK_LIBRARY_PATH="/opt/homebrew/lib" - -LINENUMS="true" mkdocs serve --dev-addr 127.0.0.1:8008 diff --git a/scripts/format.sh b/scripts/format.sh index 70c12e579d..8414381583 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -1,5 +1,5 @@ #!/bin/sh -e set -x -ruff sqlmodel tests docs_src scripts --fix +ruff check sqlmodel tests docs_src scripts --fix ruff format sqlmodel tests docs_src scripts diff --git a/scripts/lint.sh b/scripts/lint.sh index f66882239f..4b279b5fcb 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -4,5 +4,5 @@ set -e set -x mypy sqlmodel -ruff sqlmodel tests docs_src scripts +ruff check sqlmodel tests docs_src scripts ruff format sqlmodel tests docs_src --check diff --git a/scripts/publish.sh b/scripts/publish.sh deleted file mode 100755 index 7a9a127101..0000000000 --- a/scripts/publish.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -set -e - -python -m poetry publish --build diff --git a/scripts/test-files.sh b/scripts/test-files.sh deleted file mode 100755 index 36579ce7b6..0000000000 --- a/scripts/test-files.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -set -e -set -x - -# Check README.md is up to date -diff --brief docs/index.md README.md diff --git a/scripts/zip-docs.sh b/scripts/zip-docs.sh deleted file mode 100644 index 69315f5ddd..0000000000 --- a/scripts/zip-docs.sh +++ /dev/null @@ -1,11 +0,0 @@ -#! /usr/bin/env bash - -set -x -set -e - -cd ./site - -if [ -f docs.zip ]; then - rm -rf docs.zip -fi -zip -r docs.zip ./ From 32353eeb074bb7c1e50286a6f49e9780196c9e56 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 29 Apr 2024 23:26:22 +0000 Subject: [PATCH 356/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index b55bdcdc8e..e64cc01553 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -13,6 +13,7 @@ ### Internal +* 🔨 Update internal scripts and remove unused ones. PR [#914](https://github.com/tiangolo/sqlmodel/pull/914) by [@tiangolo](https://github.com/tiangolo). * 🔧 Migrate from Poetry to PDM for the internal build config. PR [#912](https://github.com/tiangolo/sqlmodel/pull/912) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update MkDocs, disable cards while I can upgrade to the latest MkDocs Material, that fixes an issue with social cards. PR [#888](https://github.com/tiangolo/sqlmodel/pull/888) by [@tiangolo](https://github.com/tiangolo). * 👷 Add cron to run test once a week on monday. PR [#869](https://github.com/tiangolo/sqlmodel/pull/869) by [@estebanx64](https://github.com/estebanx64). From 3ecbeacb469fc4ff7cdfd7bfad78df6a3764427b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 23:34:38 +0000 Subject: [PATCH 357/906] =?UTF-8?q?=E2=AC=86=20Bump=20actions/setup-python?= =?UTF-8?q?=20from=204=20to=205=20(#733)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- .github/workflows/build-docs.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/smokeshow.yml | 2 +- .github/workflows/test.yml | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 4871f7bb47..b9e1d336ac 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -46,7 +46,7 @@ jobs: run: echo "$GITHUB_CONTEXT" - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.11" - uses: actions/cache@v3 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 911cd623a7..6f57b7f8ef 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -23,7 +23,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.11" - name: Install build dependencies diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index d2c274ff29..44ce46cfe8 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: '3.9' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f0872ef623..59260fa7b0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,7 +38,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} # Allow debugging with tmate @@ -84,7 +84,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: '3.8' From 1c4f425f1744e34ad425d061668b818673f54a43 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 29 Apr 2024 23:35:03 +0000 Subject: [PATCH 358/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index e64cc01553..bec99c83fb 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -13,6 +13,7 @@ ### Internal +* ⬆ Bump actions/setup-python from 4 to 5. PR [#733](https://github.com/tiangolo/sqlmodel/pull/733) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔨 Update internal scripts and remove unused ones. PR [#914](https://github.com/tiangolo/sqlmodel/pull/914) by [@tiangolo](https://github.com/tiangolo). * 🔧 Migrate from Poetry to PDM for the internal build config. PR [#912](https://github.com/tiangolo/sqlmodel/pull/912) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update MkDocs, disable cards while I can upgrade to the latest MkDocs Material, that fixes an issue with social cards. PR [#888](https://github.com/tiangolo/sqlmodel/pull/888) by [@tiangolo](https://github.com/tiangolo). From 0431c5bb26b587a99cd0aa0efbf81e62e6f6d0d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 29 Apr 2024 16:44:21 -0700 Subject: [PATCH 359/906] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.0.?= =?UTF-8?q?17?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 2 ++ sqlmodel/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index bec99c83fb..da824db5dd 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest Changes +## 0.0.17 + ### Refactors * ♻️ Refactor types to properly support Pydantic 2.7. PR [#913](https://github.com/tiangolo/sqlmodel/pull/913) by [@tiangolo](https://github.com/tiangolo). diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index fac039e819..aa108fefe9 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.0.17.dev2" +__version__ = "0.0.17" # Re-export from SQLAlchemy from sqlalchemy.engine import create_engine as create_engine From 28d0e7637039f49553f081add72500939ad6d86c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 29 Apr 2024 17:00:40 -0700 Subject: [PATCH 360/906] =?UTF-8?q?=F0=9F=94=A7=20Re-enable=20MkDocs=20Mat?= =?UTF-8?q?erial=20Social=20plugin=20(#915)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mkdocs.insiders.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mkdocs.insiders.yml b/mkdocs.insiders.yml index f59d3f3327..d24d754930 100644 --- a/mkdocs.insiders.yml +++ b/mkdocs.insiders.yml @@ -1,4 +1,3 @@ plugins: - # TODO: Re-enable once this is fixed: https://github.com/squidfunk/mkdocs-material/issues/6983 - # social: + social: typeset: From 39cbf2790457b84c3f8abd58e95d7e79a7652e20 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 30 Apr 2024 00:01:02 +0000 Subject: [PATCH 361/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index da824db5dd..48e3eba7ce 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Internal + +* 🔧 Re-enable MkDocs Material Social plugin. PR [#915](https://github.com/tiangolo/sqlmodel/pull/915) by [@tiangolo](https://github.com/tiangolo). + ## 0.0.17 ### Refactors From 9ebbf255f71ba4dc2f17b22443e7a87d3da33b36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 29 Apr 2024 23:22:28 -0700 Subject: [PATCH 362/906] =?UTF-8?q?=E2=9C=A8=20Add=20sqlmodel-slim=20setup?= =?UTF-8?q?=20(#916)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/publish.yml | 1 + .github/workflows/test-redistribute.yml | 1 + pdm_build.py | 39 +++++++++++++++++++++++++ pyproject.toml | 12 ++++++++ sqlmodel/__init__.py | 2 +- 5 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 pdm_build.py diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6f57b7f8ef..1397e17ae6 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,6 +18,7 @@ jobs: matrix: package: - sqlmodel + - sqlmodel-slim permissions: id-token: write steps: diff --git a/.github/workflows/test-redistribute.yml b/.github/workflows/test-redistribute.yml index 45b82414c4..2994da2fe5 100644 --- a/.github/workflows/test-redistribute.yml +++ b/.github/workflows/test-redistribute.yml @@ -16,6 +16,7 @@ jobs: matrix: package: - sqlmodel + - sqlmodel-slim steps: - name: Dump GitHub context env: diff --git a/pdm_build.py b/pdm_build.py new file mode 100644 index 0000000000..2324670159 --- /dev/null +++ b/pdm_build.py @@ -0,0 +1,39 @@ +import os +from typing import Any, Dict, List + +from pdm.backend.hooks import Context + +TIANGOLO_BUILD_PACKAGE = os.getenv("TIANGOLO_BUILD_PACKAGE", "sqlmodel") + + +def pdm_build_initialize(context: Context) -> None: + metadata = context.config.metadata + # Get custom config for the current package, from the env var + config: Dict[str, Any] = context.config.data["tool"]["tiangolo"][ + "_internal-slim-build" + ]["packages"][TIANGOLO_BUILD_PACKAGE] + project_config: Dict[str, Any] = config["project"] + # Get main optional dependencies, extras + optional_dependencies: Dict[str, List[str]] = metadata.get( + "optional-dependencies", {} + ) + # Get custom optional dependencies name to always include in this (non-slim) package + include_optional_dependencies: List[str] = config.get( + "include-optional-dependencies", [] + ) + # Override main [project] configs with custom configs for this package + for key, value in project_config.items(): + metadata[key] = value + # Get custom build config for the current package + build_config: Dict[str, Any] = ( + config.get("tool", {}).get("pdm", {}).get("build", {}) + ) + # Override PDM build config with custom build config for this package + for key, value in build_config.items(): + context.config.build_config[key] = value + # Get main dependencies + dependencies: List[str] = metadata.get("dependencies", []) + # Add optional dependencies to the default dependencies for this (non-slim) package + for include_optional in include_optional_dependencies: + optional_dependencies_group = optional_dependencies.get(include_optional, []) + dependencies.extend(optional_dependencies_group) diff --git a/pyproject.toml b/pyproject.toml index 17f72941b8..ade520ab48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,6 +57,18 @@ source-includes = [ "sqlmodel/sql/expression.py.jinja2", ] +[tool.tiangolo._internal-slim-build.packages.sqlmodel-slim.project] +name = "sqlmodel-slim" + +[tool.tiangolo._internal-slim-build.packages.sqlmodel] +# include-optional-dependencies = ["standard"] + +[tool.tiangolo._internal-slim-build.packages.sqlmodel.project] +optional-dependencies = {} + +# [tool.tiangolo._internal-slim-build.packages.sqlmodel.project.scripts] +# sqlmodel = "sqlmodel.cli:main" + [tool.coverage.run] parallel = true source = [ diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index aa108fefe9..39a8d46418 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.0.17" +__version__ = "0.0.18.dev1" # Re-export from SQLAlchemy from sqlalchemy.engine import create_engine as create_engine From a280b58c1051511d1dbbac90ad5cb02bf8121caf Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 30 Apr 2024 06:22:46 +0000 Subject: [PATCH 363/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 48e3eba7ce..ddede18e0e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Internal +* ✨ Add sqlmodel-slim setup. PR [#916](https://github.com/tiangolo/sqlmodel/pull/916) by [@tiangolo](https://github.com/tiangolo). * 🔧 Re-enable MkDocs Material Social plugin. PR [#915](https://github.com/tiangolo/sqlmodel/pull/915) by [@tiangolo](https://github.com/tiangolo). ## 0.0.17 From 900e0d3371dd8d1e2a8d5665c4255ca7d9625896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 29 Apr 2024 23:25:02 -0700 Subject: [PATCH 364/906] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.0.?= =?UTF-8?q?18?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 2 ++ sqlmodel/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index ddede18e0e..3235142562 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest Changes +## 0.0.18 + ### Internal * ✨ Add sqlmodel-slim setup. PR [#916](https://github.com/tiangolo/sqlmodel/pull/916) by [@tiangolo](https://github.com/tiangolo). diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index 39a8d46418..397c07f5d2 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.0.18.dev1" +__version__ = "0.0.18" # Re-export from SQLAlchemy from sqlalchemy.engine import create_engine as create_engine From c13b71056e10dc183f6837acb3204fb179cc4b24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 29 Apr 2024 23:29:21 -0700 Subject: [PATCH 365/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 3235142562..5f62c34633 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,7 +6,10 @@ ### Internal -* ✨ Add sqlmodel-slim setup. PR [#916](https://github.com/tiangolo/sqlmodel/pull/916) by [@tiangolo](https://github.com/tiangolo). +* ✨ Add `sqlmodel-slim` setup. PR [#916](https://github.com/tiangolo/sqlmodel/pull/916) by [@tiangolo](https://github.com/tiangolo). + +In the future SQLModel will include the standard default recommended packages, and `sqlmodel-slim` will come without those recommended standard packages and with a group of optional dependencies `sqlmodel-slim[standard]`, equivalent to `sqlmodel`, for those that want to opt out of those packages. + * 🔧 Re-enable MkDocs Material Social plugin. PR [#915](https://github.com/tiangolo/sqlmodel/pull/915) by [@tiangolo](https://github.com/tiangolo). ## 0.0.17 From 5e592c9a0d59d8c08f7dc25bc7617dc195bc1cd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 7 May 2024 11:32:16 -0700 Subject: [PATCH 366/906] =?UTF-8?q?=F0=9F=91=B7=20Tweak=20CI=20for=20test-?= =?UTF-8?q?redistribute,=20add=20needed=20env=20vars=20for=20slim=20(#929)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test-redistribute.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test-redistribute.yml b/.github/workflows/test-redistribute.yml index 2994da2fe5..dc86da69c0 100644 --- a/.github/workflows/test-redistribute.yml +++ b/.github/workflows/test-redistribute.yml @@ -41,6 +41,8 @@ jobs: run: | cd dist/sqlmodel*/ pip install -r requirements-tests.txt + env: + TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }} - name: Run source distribution tests run: | cd dist/sqlmodel*/ From df0f834227cff41f18eba35db6a242a42671e893 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 7 May 2024 18:32:44 +0000 Subject: [PATCH 367/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 5f62c34633..9c14895b34 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Internal + +* 👷 Tweak CI for test-redistribute, add needed env vars for slim. PR [#929](https://github.com/tiangolo/sqlmodel/pull/929) by [@tiangolo](https://github.com/tiangolo). + ## 0.0.18 ### Internal From e4013acc548f809001179fd48192052e927547a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 10 May 2024 14:00:24 -0700 Subject: [PATCH 368/906] =?UTF-8?q?=F0=9F=91=B7=20Update=20GitHub=20Action?= =?UTF-8?q?s=20to=20download=20and=20upload=20artifacts=20(#936)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 2 +- .github/workflows/deploy-docs.yml | 16 +++++++--------- .github/workflows/smokeshow.yml | 10 ++++++---- .github/workflows/test.yml | 11 ++++++----- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index b9e1d336ac..16da4a2da3 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -71,7 +71,7 @@ jobs: run: python ./scripts/docs.py verify-readme - name: Build Docs run: python ./scripts/docs.py build - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: docs-site path: ./site/** diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index f9035d89a7..41320b57b1 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -19,18 +19,16 @@ jobs: run: | rm -rf ./site mkdir ./site - - name: Download Artifact Docs - id: download - uses: dawidd6/action-download-artifact@v2.28.0 + - uses: actions/download-artifact@v4 with: - if_no_artifact_found: ignore - github_token: ${{ secrets.GITHUB_TOKEN }} - workflow: build-docs.yml - run_id: ${{ github.event.workflow_run.id }} - name: docs-site path: ./site/ + pattern: docs-site + merge-multiple: true + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} - name: Deploy to Cloudflare Pages - if: steps.download.outputs.found_artifact == 'true' + # hashFiles returns an empty string if there are no files + if: hashFiles('./site/*') id: deploy uses: cloudflare/pages-action@v1 with: diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index 44ce46cfe8..bc37a92e78 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -20,12 +20,14 @@ jobs: - run: pip install smokeshow - - uses: dawidd6/action-download-artifact@v2.28.0 + - uses: actions/download-artifact@v4 with: - workflow: test.yml - commit: ${{ github.event.workflow_run.head_sha }} + name: coverage-html + path: htmlcov + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} - - run: smokeshow upload coverage-html + - run: smokeshow upload htmlcov env: SMOKESHOW_GITHUB_STATUS_DESCRIPTION: Coverage {coverage-percentage} SMOKESHOW_GITHUB_COVERAGE_THRESHOLD: 95 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 59260fa7b0..70e64094a4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -72,9 +72,9 @@ jobs: COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }} CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }} - name: Store coverage files - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: coverage + name: coverage-${{ matrix.python-version }}-${{ matrix.pydantic-version }} path: coverage coverage-combine: needs: @@ -89,10 +89,11 @@ jobs: python-version: '3.8' - name: Get coverage files - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: - name: coverage + pattern: coverage-* path: coverage + merge-multiple: true - run: pip install coverage[toml] @@ -102,7 +103,7 @@ jobs: - run: coverage html --show-contexts --title "Coverage for ${{ github.sha }}" - name: Store coverage HTML - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: coverage-html path: htmlcov From dcf4f58e8176df0200ad79a9efc3b75d29ebb9a9 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 10 May 2024 21:00:41 +0000 Subject: [PATCH 369/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 9c14895b34..5b7d2b7e3c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Internal +* 👷 Update GitHub Actions to download and upload artifacts. PR [#936](https://github.com/tiangolo/sqlmodel/pull/936) by [@tiangolo](https://github.com/tiangolo). * 👷 Tweak CI for test-redistribute, add needed env vars for slim. PR [#929](https://github.com/tiangolo/sqlmodel/pull/929) by [@tiangolo](https://github.com/tiangolo). ## 0.0.18 From 662bd641b821b6724763f8503b03481459b6e1cf Mon Sep 17 00:00:00 2001 From: Soof Golan <83900570+soof-golan@users.noreply.github.com> Date: Tue, 4 Jun 2024 02:56:30 +0300 Subject: [PATCH 370/906] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20broken=20lin?= =?UTF-8?q?k=20to=20`@dataclass=5Ftransform`=20(now=20PEP=20681)=20in=20`d?= =?UTF-8?q?ocs/features.md`=20(#753)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez Co-authored-by: Patrick Arminio --- docs/features.md | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/docs/features.md b/docs/features.md index f84606b9b5..f0d56925c7 100644 --- a/docs/features.md +++ b/docs/features.md @@ -36,20 +36,10 @@ You will get completion for everything while writing the **minimum** amount of c You won't need to keep guessing the types of different attributes in your models, if they could be `None`, etc. Your editor will be able to help you with everything because **SQLModel** is based on **standard Python type annotations**. -**SQLModel** even adopts currently in development standards for Python type annotations to ensure the **best developer experience**, so you will get inline errors and autocompletion even while creating new model instances. +**SQLModel** adopts PEP 681 for Python type annotations to ensure the **best developer experience**, so you will get inline errors and autocompletion even while creating new model instances. -/// info - -Don't worry, adopting this in-development standard only affects/improves editor support. - -It doesn't affect performance or correctness. And if the in-progress standard was deprecated your code won't be affected. - -Meanwhile, you will get inline errors (like type checks) and autocompletion on places you wouldn't get with any other library. 🎉 - -/// - ## Short **SQLModel** has **sensible defaults** for everything, with **optional configurations** everywhere. From a319952be1cd80ba4a9e0ccc0d74cd1279aa59f4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 3 Jun 2024 23:56:49 +0000 Subject: [PATCH 371/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 5b7d2b7e3c..41b389bffd 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Docs + +* ✏️ Fix broken link to `@dataclass_transform` (now PEP 681) in `docs/features.md`. PR [#753](https://github.com/tiangolo/sqlmodel/pull/753) by [@soof-golan](https://github.com/soof-golan). + ### Internal * 👷 Update GitHub Actions to download and upload artifacts. PR [#936](https://github.com/tiangolo/sqlmodel/pull/936) by [@tiangolo](https://github.com/tiangolo). From 5bb4cffd4984647a2cba12ff94c5a1b9b577cf69 Mon Sep 17 00:00:00 2001 From: Esteban Maya Date: Mon, 3 Jun 2024 19:39:23 -0500 Subject: [PATCH 372/906] =?UTF-8?q?=F0=9F=90=9B=20Fix=20set=20varchar=20li?= =?UTF-8?q?mit=20when=20`max=5Flength`=20is=20set=20on=20Pydantic=20models?= =?UTF-8?q?=20using=20Pydantic=20v2=20(#963)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- sqlmodel/_compat.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index 72ec8330fd..d42a62429e 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -72,6 +72,7 @@ def partial_init() -> Generator[None, None, None]: if IS_PYDANTIC_V2: + from annotated_types import MaxLen from pydantic import ConfigDict as BaseConfig from pydantic._internal._fields import PydanticMetadata from pydantic._internal._model_construction import ModelMetaclass @@ -201,7 +202,7 @@ def get_type_from_field(field: Any) -> Any: def get_field_metadata(field: Any) -> Any: for meta in field.metadata: - if isinstance(meta, PydanticMetadata): + if isinstance(meta, (PydanticMetadata, MaxLen)): return meta return FakeMetadata() From 866d9ecb29b0913f36223aeed476d3b6fe99cde9 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 4 Jun 2024 00:39:40 +0000 Subject: [PATCH 373/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 41b389bffd..9b97f7ba88 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Fixes + +* 🐛 Fix set varchar limit when `max_length` is set on Pydantic models using Pydantic v2. PR [#963](https://github.com/tiangolo/sqlmodel/pull/963) by [@estebanx64](https://github.com/estebanx64). + ### Docs * ✏️ Fix broken link to `@dataclass_transform` (now PEP 681) in `docs/features.md`. PR [#753](https://github.com/tiangolo/sqlmodel/pull/753) by [@soof-golan](https://github.com/soof-golan). From 1b275bd6a784a99f467c702ad35e551513e2aa4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 3 Jun 2024 20:34:21 -0500 Subject: [PATCH 374/906] =?UTF-8?q?=F0=9F=93=8C=20Pin=20typing-extensions?= =?UTF-8?q?=20in=20tests=20for=20compatiblity=20with=20Python=203.8,=20dir?= =?UTF-8?q?ty-equals,=20Pydantic=20(#965)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 4 ++-- requirements-tests.txt | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 70e64094a4..a1c9b36e1c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -51,7 +51,7 @@ jobs: id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }} + key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-v01 - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: pip install -r requirements-tests.txt @@ -60,7 +60,7 @@ jobs: run: pip install --upgrade "pydantic>=1.10.0,<2.0.0" - name: Install Pydantic v2 if: matrix.pydantic-version == 'pydantic-v2' - run: pip install --upgrade "pydantic>=2.0.2,<3.0.0" + run: pip install --upgrade "pydantic>=2.0.2,<3.0.0" "typing-extensions==4.6.1" - name: Lint # Do not run on Python 3.7 as mypy behaves differently if: matrix.python-version != '3.7' && matrix.pydantic-version == 'pydantic-v2' diff --git a/requirements-tests.txt b/requirements-tests.txt index 648f99b1c9..3c2578e9c5 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -10,3 +10,6 @@ httpx ==0.24.1 # TODO: upgrade when deprecating Python 3.7 dirty-equals ==0.6.0 jinja2 ==3.1.3 +# Pin typing-extensions until Python 3.8 is deprecated or the issue with dirty-equals +# is fixed, maybe fixed after dropping Python 3.7 and upgrading dirty-equals +typing-extensions ==4.6.1 From 71de44daba6201a653211aef60342f5c9834d6d8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 4 Jun 2024 01:34:41 +0000 Subject: [PATCH 375/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 9b97f7ba88..de348cf2d7 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -12,6 +12,7 @@ ### Internal +* 📌 Pin typing-extensions in tests for compatiblity with Python 3.8, dirty-equals, Pydantic. PR [#965](https://github.com/tiangolo/sqlmodel/pull/965) by [@tiangolo](https://github.com/tiangolo). * 👷 Update GitHub Actions to download and upload artifacts. PR [#936](https://github.com/tiangolo/sqlmodel/pull/936) by [@tiangolo](https://github.com/tiangolo). * 👷 Tweak CI for test-redistribute, add needed env vars for slim. PR [#929](https://github.com/tiangolo/sqlmodel/pull/929) by [@tiangolo](https://github.com/tiangolo). From bd1641c9a21cadd5f1df3401541675b230f4a87d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 3 Jun 2024 20:39:07 -0500 Subject: [PATCH 376/906] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20minimum?= =?UTF-8?q?=20SQLAlchemy=20version=20to=202.0.14=20as=20that=20one=20inclu?= =?UTF-8?q?des=20`TryCast`=20used=20internally=20(#964)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ade520ab48..3d6f1c291d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ classifiers = [ ] dependencies = [ - "SQLAlchemy >=2.0.0,<2.1.0", + "SQLAlchemy >=2.0.14,<2.1.0", "pydantic >=1.10.13,<3.0.0", ] From d5cba6e35842dcfcbbb1273ce9dc50a87252bb58 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 4 Jun 2024 01:39:25 +0000 Subject: [PATCH 377/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index de348cf2d7..9d9d6f5fff 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,6 +6,10 @@ * 🐛 Fix set varchar limit when `max_length` is set on Pydantic models using Pydantic v2. PR [#963](https://github.com/tiangolo/sqlmodel/pull/963) by [@estebanx64](https://github.com/estebanx64). +### Upgrades + +* ⬆️ Update minimum SQLAlchemy version to 2.0.14 as that one includes `TryCast` used internally. PR [#964](https://github.com/tiangolo/sqlmodel/pull/964) by [@tiangolo](https://github.com/tiangolo). + ### Docs * ✏️ Fix broken link to `@dataclass_transform` (now PEP 681) in `docs/features.md`. PR [#753](https://github.com/tiangolo/sqlmodel/pull/753) by [@soof-golan](https://github.com/soof-golan). From d165e4b5ad20ea9ecc5f4791b5a78ffd2b3aaeda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 3 Jun 2024 21:34:54 -0500 Subject: [PATCH 378/906] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20generat?= =?UTF-8?q?e=20select=20template=20to=20isolate=20templated=20code=20to=20?= =?UTF-8?q?the=20minimum=20(#967)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- scripts/generate_select.py | 8 +- sqlmodel/sql/_expression_select_cls.py | 43 ++ sqlmodel/sql/_expression_select_gen.py | 396 ++++++++++++++++ sqlmodel/sql/_expression_select_gen.py.jinja2 | 86 ++++ sqlmodel/sql/expression.py | 430 +----------------- sqlmodel/sql/expression.py.jinja2 | 309 ------------- 7 files changed, 544 insertions(+), 730 deletions(-) create mode 100644 sqlmodel/sql/_expression_select_cls.py create mode 100644 sqlmodel/sql/_expression_select_gen.py create mode 100644 sqlmodel/sql/_expression_select_gen.py.jinja2 delete mode 100644 sqlmodel/sql/expression.py.jinja2 diff --git a/pyproject.toml b/pyproject.toml index 3d6f1c291d..14a1432ce7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,7 +90,7 @@ exclude_lines = [ strict = true [[tool.mypy.overrides]] -module = "sqlmodel.sql.expression" +module = "sqlmodel.sql._expression_select_gen" warn_unused_ignores = false [[tool.mypy.overrides]] diff --git a/scripts/generate_select.py b/scripts/generate_select.py index 8f01cb808d..49c2b2b015 100644 --- a/scripts/generate_select.py +++ b/scripts/generate_select.py @@ -7,8 +7,10 @@ from jinja2 import Template from pydantic import BaseModel -template_path = Path(__file__).parent.parent / "sqlmodel/sql/expression.py.jinja2" -destiny_path = Path(__file__).parent.parent / "sqlmodel/sql/expression.py" +template_path = ( + Path(__file__).parent.parent / "sqlmodel/sql/_expression_select_gen.py.jinja2" +) +destiny_path = Path(__file__).parent.parent / "sqlmodel/sql/_expression_select_gen.py" number_of_types = 4 @@ -48,7 +50,7 @@ class Arg(BaseModel): result = ( "# WARNING: do not modify this code, it is generated by " - "expression.py.jinja2\n\n" + result + "_expression_select_gen.py.jinja2\n\n" + result ) result = black.format_str(result, mode=black.Mode()) diff --git a/sqlmodel/sql/_expression_select_cls.py b/sqlmodel/sql/_expression_select_cls.py new file mode 100644 index 0000000000..9fd8609956 --- /dev/null +++ b/sqlmodel/sql/_expression_select_cls.py @@ -0,0 +1,43 @@ +from typing import ( + Tuple, + TypeVar, + Union, +) + +from sqlalchemy.sql._typing import ( + _ColumnExpressionArgument, +) +from sqlalchemy.sql.expression import Select as _Select +from typing_extensions import Self + +_T = TypeVar("_T") + + +# Separate this class in SelectBase, Select, and SelectOfScalar so that they can share +# where and having without having type overlap incompatibility in session.exec(). +class SelectBase(_Select[Tuple[_T]]): + inherit_cache = True + + def where(self, *whereclause: Union[_ColumnExpressionArgument[bool], bool]) -> Self: + """Return a new `Select` construct with the given expression added to + its `WHERE` clause, joined to the existing clause via `AND`, if any. + """ + return super().where(*whereclause) # type: ignore[arg-type] + + def having(self, *having: Union[_ColumnExpressionArgument[bool], bool]) -> Self: + """Return a new `Select` construct with the given expression added to + its `HAVING` clause, joined to the existing clause via `AND`, if any. + """ + return super().having(*having) # type: ignore[arg-type] + + +class Select(SelectBase[_T]): + inherit_cache = True + + +# This is not comparable to sqlalchemy.sql.selectable.ScalarSelect, that has a different +# purpose. This is the same as a normal SQLAlchemy Select class where there's only one +# entity, so the result will be converted to a scalar by default. This way writing +# for loops on the results will feel natural. +class SelectOfScalar(SelectBase[_T]): + inherit_cache = True diff --git a/sqlmodel/sql/_expression_select_gen.py b/sqlmodel/sql/_expression_select_gen.py new file mode 100644 index 0000000000..b6c15742fa --- /dev/null +++ b/sqlmodel/sql/_expression_select_gen.py @@ -0,0 +1,396 @@ +# WARNING: do not modify this code, it is generated by _expression_select_gen.py.jinja2 + +from datetime import datetime +from typing import ( + Any, + Mapping, + Sequence, + Tuple, + Type, + TypeVar, + Union, + overload, +) +from uuid import UUID + +from sqlalchemy import ( + Column, +) +from sqlalchemy.sql.elements import ( + SQLCoreOperations, +) +from sqlalchemy.sql.roles import TypedColumnsClauseRole + +from ._expression_select_cls import Select, SelectOfScalar + +_T = TypeVar("_T") + + +_TCCA = Union[ + TypedColumnsClauseRole[_T], + SQLCoreOperations[_T], + Type[_T], +] + +# Generated TypeVars start + + +_TScalar_0 = TypeVar( + "_TScalar_0", + Column, # type: ignore + Sequence, # type: ignore + Mapping, # type: ignore + UUID, + datetime, + float, + int, + bool, + bytes, + str, + None, +) + +_T0 = TypeVar("_T0") + + +_TScalar_1 = TypeVar( + "_TScalar_1", + Column, # type: ignore + Sequence, # type: ignore + Mapping, # type: ignore + UUID, + datetime, + float, + int, + bool, + bytes, + str, + None, +) + +_T1 = TypeVar("_T1") + + +_TScalar_2 = TypeVar( + "_TScalar_2", + Column, # type: ignore + Sequence, # type: ignore + Mapping, # type: ignore + UUID, + datetime, + float, + int, + bool, + bytes, + str, + None, +) + +_T2 = TypeVar("_T2") + + +_TScalar_3 = TypeVar( + "_TScalar_3", + Column, # type: ignore + Sequence, # type: ignore + Mapping, # type: ignore + UUID, + datetime, + float, + int, + bool, + bytes, + str, + None, +) + +_T3 = TypeVar("_T3") + + +# Generated TypeVars end + + +@overload +def select(__ent0: _TCCA[_T0]) -> SelectOfScalar[_T0]: + ... + + +@overload +def select(__ent0: _TScalar_0) -> SelectOfScalar[_TScalar_0]: # type: ignore + ... + + +# Generated overloads start + + +@overload +def select( # type: ignore + __ent0: _TCCA[_T0], + __ent1: _TCCA[_T1], +) -> Select[Tuple[_T0, _T1]]: + ... + + +@overload +def select( # type: ignore + __ent0: _TCCA[_T0], + entity_1: _TScalar_1, +) -> Select[Tuple[_T0, _TScalar_1]]: + ... + + +@overload +def select( # type: ignore + entity_0: _TScalar_0, + __ent1: _TCCA[_T1], +) -> Select[Tuple[_TScalar_0, _T1]]: + ... + + +@overload +def select( # type: ignore + entity_0: _TScalar_0, + entity_1: _TScalar_1, +) -> Select[Tuple[_TScalar_0, _TScalar_1]]: + ... + + +@overload +def select( # type: ignore + __ent0: _TCCA[_T0], + __ent1: _TCCA[_T1], + __ent2: _TCCA[_T2], +) -> Select[Tuple[_T0, _T1, _T2]]: + ... + + +@overload +def select( # type: ignore + __ent0: _TCCA[_T0], + __ent1: _TCCA[_T1], + entity_2: _TScalar_2, +) -> Select[Tuple[_T0, _T1, _TScalar_2]]: + ... + + +@overload +def select( # type: ignore + __ent0: _TCCA[_T0], + entity_1: _TScalar_1, + __ent2: _TCCA[_T2], +) -> Select[Tuple[_T0, _TScalar_1, _T2]]: + ... + + +@overload +def select( # type: ignore + __ent0: _TCCA[_T0], + entity_1: _TScalar_1, + entity_2: _TScalar_2, +) -> Select[Tuple[_T0, _TScalar_1, _TScalar_2]]: + ... + + +@overload +def select( # type: ignore + entity_0: _TScalar_0, + __ent1: _TCCA[_T1], + __ent2: _TCCA[_T2], +) -> Select[Tuple[_TScalar_0, _T1, _T2]]: + ... + + +@overload +def select( # type: ignore + entity_0: _TScalar_0, + __ent1: _TCCA[_T1], + entity_2: _TScalar_2, +) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2]]: + ... + + +@overload +def select( # type: ignore + entity_0: _TScalar_0, + entity_1: _TScalar_1, + __ent2: _TCCA[_T2], +) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2]]: + ... + + +@overload +def select( # type: ignore + entity_0: _TScalar_0, + entity_1: _TScalar_1, + entity_2: _TScalar_2, +) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2]]: + ... + + +@overload +def select( # type: ignore + __ent0: _TCCA[_T0], + __ent1: _TCCA[_T1], + __ent2: _TCCA[_T2], + __ent3: _TCCA[_T3], +) -> Select[Tuple[_T0, _T1, _T2, _T3]]: + ... + + +@overload +def select( # type: ignore + __ent0: _TCCA[_T0], + __ent1: _TCCA[_T1], + __ent2: _TCCA[_T2], + entity_3: _TScalar_3, +) -> Select[Tuple[_T0, _T1, _T2, _TScalar_3]]: + ... + + +@overload +def select( # type: ignore + __ent0: _TCCA[_T0], + __ent1: _TCCA[_T1], + entity_2: _TScalar_2, + __ent3: _TCCA[_T3], +) -> Select[Tuple[_T0, _T1, _TScalar_2, _T3]]: + ... + + +@overload +def select( # type: ignore + __ent0: _TCCA[_T0], + __ent1: _TCCA[_T1], + entity_2: _TScalar_2, + entity_3: _TScalar_3, +) -> Select[Tuple[_T0, _T1, _TScalar_2, _TScalar_3]]: + ... + + +@overload +def select( # type: ignore + __ent0: _TCCA[_T0], + entity_1: _TScalar_1, + __ent2: _TCCA[_T2], + __ent3: _TCCA[_T3], +) -> Select[Tuple[_T0, _TScalar_1, _T2, _T3]]: + ... + + +@overload +def select( # type: ignore + __ent0: _TCCA[_T0], + entity_1: _TScalar_1, + __ent2: _TCCA[_T2], + entity_3: _TScalar_3, +) -> Select[Tuple[_T0, _TScalar_1, _T2, _TScalar_3]]: + ... + + +@overload +def select( # type: ignore + __ent0: _TCCA[_T0], + entity_1: _TScalar_1, + entity_2: _TScalar_2, + __ent3: _TCCA[_T3], +) -> Select[Tuple[_T0, _TScalar_1, _TScalar_2, _T3]]: + ... + + +@overload +def select( # type: ignore + __ent0: _TCCA[_T0], + entity_1: _TScalar_1, + entity_2: _TScalar_2, + entity_3: _TScalar_3, +) -> Select[Tuple[_T0, _TScalar_1, _TScalar_2, _TScalar_3]]: + ... + + +@overload +def select( # type: ignore + entity_0: _TScalar_0, + __ent1: _TCCA[_T1], + __ent2: _TCCA[_T2], + __ent3: _TCCA[_T3], +) -> Select[Tuple[_TScalar_0, _T1, _T2, _T3]]: + ... + + +@overload +def select( # type: ignore + entity_0: _TScalar_0, + __ent1: _TCCA[_T1], + __ent2: _TCCA[_T2], + entity_3: _TScalar_3, +) -> Select[Tuple[_TScalar_0, _T1, _T2, _TScalar_3]]: + ... + + +@overload +def select( # type: ignore + entity_0: _TScalar_0, + __ent1: _TCCA[_T1], + entity_2: _TScalar_2, + __ent3: _TCCA[_T3], +) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2, _T3]]: + ... + + +@overload +def select( # type: ignore + entity_0: _TScalar_0, + __ent1: _TCCA[_T1], + entity_2: _TScalar_2, + entity_3: _TScalar_3, +) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2, _TScalar_3]]: + ... + + +@overload +def select( # type: ignore + entity_0: _TScalar_0, + entity_1: _TScalar_1, + __ent2: _TCCA[_T2], + __ent3: _TCCA[_T3], +) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2, _T3]]: + ... + + +@overload +def select( # type: ignore + entity_0: _TScalar_0, + entity_1: _TScalar_1, + __ent2: _TCCA[_T2], + entity_3: _TScalar_3, +) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2, _TScalar_3]]: + ... + + +@overload +def select( # type: ignore + entity_0: _TScalar_0, + entity_1: _TScalar_1, + entity_2: _TScalar_2, + __ent3: _TCCA[_T3], +) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2, _T3]]: + ... + + +@overload +def select( # type: ignore + entity_0: _TScalar_0, + entity_1: _TScalar_1, + entity_2: _TScalar_2, + entity_3: _TScalar_3, +) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2, _TScalar_3]]: + ... + + +# Generated overloads end + + +def select(*entities: Any) -> Union[Select, SelectOfScalar]: # type: ignore + if len(entities) == 1: + return SelectOfScalar(*entities) + return Select(*entities) diff --git a/sqlmodel/sql/_expression_select_gen.py.jinja2 b/sqlmodel/sql/_expression_select_gen.py.jinja2 new file mode 100644 index 0000000000..307e32b784 --- /dev/null +++ b/sqlmodel/sql/_expression_select_gen.py.jinja2 @@ -0,0 +1,86 @@ +from datetime import datetime +from typing import ( + Any, + Mapping, + Sequence, + Tuple, + Type, + TypeVar, + Union, + overload, +) +from uuid import UUID + +from sqlalchemy import ( + Column, +) +from sqlalchemy.sql.elements import ( + SQLCoreOperations, +) +from sqlalchemy.sql.roles import TypedColumnsClauseRole + +from ._expression_select_cls import Select, SelectOfScalar + +_T = TypeVar("_T") + + +_TCCA = Union[ + TypedColumnsClauseRole[_T], + SQLCoreOperations[_T], + Type[_T], +] + +# Generated TypeVars start + + +{% for i in range(number_of_types) %} +_TScalar_{{ i }} = TypeVar( + "_TScalar_{{ i }}", + Column, # type: ignore + Sequence, # type: ignore + Mapping, # type: ignore + UUID, + datetime, + float, + int, + bool, + bytes, + str, + None, +) + +_T{{ i }} = TypeVar("_T{{ i }}") + +{% endfor %} + +# Generated TypeVars end + +@overload +def select(__ent0: _TCCA[_T0]) -> SelectOfScalar[_T0]: + ... + + +@overload +def select(__ent0: _TScalar_0) -> SelectOfScalar[_TScalar_0]: # type: ignore + ... + + +# Generated overloads start + +{% for signature in signatures %} + +@overload +def select( # type: ignore + {% for arg in signature[0] %}{{ arg.name }}: {{ arg.annotation }}, {% endfor %} + ) -> Select[Tuple[{%for ret in signature[1] %}{{ ret }} {% if not loop.last %}, {% endif %}{% endfor %}]]: + ... + +{% endfor %} + +# Generated overloads end + + +def select(*entities: Any) -> Union[Select, SelectOfScalar]: # type: ignore + if len(entities) == 1: + return SelectOfScalar(*entities) + return Select(*entities) diff --git a/sqlmodel/sql/expression.py b/sqlmodel/sql/expression.py index 112968c655..f431747670 100644 --- a/sqlmodel/sql/expression.py +++ b/sqlmodel/sql/expression.py @@ -1,6 +1,3 @@ -# WARNING: do not modify this code, it is generated by expression.py.jinja2 - -from datetime import datetime from typing import ( Any, Iterable, @@ -11,9 +8,7 @@ Type, TypeVar, Union, - overload, ) -from uuid import UUID import sqlalchemy from sqlalchemy import ( @@ -39,14 +34,15 @@ Cast, CollectionAggregate, ColumnClause, - SQLCoreOperations, TryCast, UnaryExpression, ) -from sqlalchemy.sql.expression import Select as _Select -from sqlalchemy.sql.roles import TypedColumnsClauseRole from sqlalchemy.sql.type_api import TypeEngine -from typing_extensions import Literal, Self +from typing_extensions import Literal + +from ._expression_select_cls import Select as Select +from ._expression_select_cls import SelectOfScalar as SelectOfScalar +from ._expression_select_gen import select as select _T = TypeVar("_T") @@ -89,7 +85,7 @@ def between( upper_bound: Any, symmetric: bool = False, ) -> BinaryExpression[bool]: - return sqlalchemy.between(expr, lower_bound, upper_bound, symmetric=symmetric) # type: ignore[arg-type] + return sqlalchemy.between(expr, lower_bound, upper_bound, symmetric=symmetric) def not_(clause: Union[_ColumnExpressionArgument[_T], _T]) -> ColumnElement[_T]: @@ -110,14 +106,14 @@ def cast( expression: Union[_ColumnExpressionOrLiteralArgument[Any], Any], type_: "_TypeEngineArgument[_T]", ) -> Cast[_T]: - return sqlalchemy.cast(expression, type_) # type: ignore[arg-type] + return sqlalchemy.cast(expression, type_) def try_cast( expression: Union[_ColumnExpressionOrLiteralArgument[Any], Any], type_: "_TypeEngineArgument[_T]", ) -> TryCast[_T]: - return sqlalchemy.try_cast(expression, type_) # type: ignore[arg-type] + return sqlalchemy.try_cast(expression, type_) def desc( @@ -135,7 +131,7 @@ def bitwise_not(expr: Union[_ColumnExpressionArgument[_T], _T]) -> UnaryExpressi def extract(field: str, expr: Union[_ColumnExpressionArgument[Any], Any]) -> Extract: - return sqlalchemy.extract(field, expr) # type: ignore[arg-type] + return sqlalchemy.extract(field, expr) def funcfilter( @@ -162,7 +158,7 @@ def nulls_last(column: Union[_ColumnExpressionArgument[_T], _T]) -> UnaryExpress return sqlalchemy.nulls_last(column) # type: ignore[arg-type] -def or_( # type: ignore[empty-body] +def or_( initial_clause: Union[Literal[False], _ColumnExpressionArgument[bool], bool], *clauses: Union[_ColumnExpressionArgument[bool], bool], ) -> ColumnElement[bool]: @@ -190,7 +186,7 @@ def over( ) -> Over[_T]: return sqlalchemy.over( element, partition_by=partition_by, order_by=order_by, range_=range_, rows=rows - ) # type: ignore[arg-type] + ) def tuple_( @@ -204,413 +200,13 @@ def type_coerce( expression: Union[_ColumnExpressionOrLiteralArgument[Any], Any], type_: "_TypeEngineArgument[_T]", ) -> TypeCoerce[_T]: - return sqlalchemy.type_coerce(expression, type_) # type: ignore[arg-type] + return sqlalchemy.type_coerce(expression, type_) def within_group( element: FunctionElement[_T], *order_by: Union[_ColumnExpressionArgument[Any], Any] ) -> WithinGroup[_T]: - return sqlalchemy.within_group(element, *order_by) # type: ignore[arg-type] - - -# Separate this class in SelectBase, Select, and SelectOfScalar so that they can share -# where and having without having type overlap incompatibility in session.exec(). -class SelectBase(_Select[Tuple[_T]]): - inherit_cache = True - - def where(self, *whereclause: Union[_ColumnExpressionArgument[bool], bool]) -> Self: - """Return a new `Select` construct with the given expression added to - its `WHERE` clause, joined to the existing clause via `AND`, if any. - """ - return super().where(*whereclause) # type: ignore[arg-type] - - def having(self, *having: Union[_ColumnExpressionArgument[bool], bool]) -> Self: - """Return a new `Select` construct with the given expression added to - its `HAVING` clause, joined to the existing clause via `AND`, if any. - """ - return super().having(*having) # type: ignore[arg-type] - - -class Select(SelectBase[_T]): - inherit_cache = True - - -# This is not comparable to sqlalchemy.sql.selectable.ScalarSelect, that has a different -# purpose. This is the same as a normal SQLAlchemy Select class where there's only one -# entity, so the result will be converted to a scalar by default. This way writing -# for loops on the results will feel natural. -class SelectOfScalar(SelectBase[_T]): - inherit_cache = True - - -_TCCA = Union[ - TypedColumnsClauseRole[_T], - SQLCoreOperations[_T], - Type[_T], -] - -# Generated TypeVars start - - -_TScalar_0 = TypeVar( - "_TScalar_0", - Column, # type: ignore - Sequence, # type: ignore - Mapping, # type: ignore - UUID, - datetime, - float, - int, - bool, - bytes, - str, - None, -) - -_T0 = TypeVar("_T0") - - -_TScalar_1 = TypeVar( - "_TScalar_1", - Column, # type: ignore - Sequence, # type: ignore - Mapping, # type: ignore - UUID, - datetime, - float, - int, - bool, - bytes, - str, - None, -) - -_T1 = TypeVar("_T1") - - -_TScalar_2 = TypeVar( - "_TScalar_2", - Column, # type: ignore - Sequence, # type: ignore - Mapping, # type: ignore - UUID, - datetime, - float, - int, - bool, - bytes, - str, - None, -) - -_T2 = TypeVar("_T2") - - -_TScalar_3 = TypeVar( - "_TScalar_3", - Column, # type: ignore - Sequence, # type: ignore - Mapping, # type: ignore - UUID, - datetime, - float, - int, - bool, - bytes, - str, - None, -) - -_T3 = TypeVar("_T3") - - -# Generated TypeVars end - - -@overload -def select(__ent0: _TCCA[_T0]) -> SelectOfScalar[_T0]: - ... - - -@overload -def select(__ent0: _TScalar_0) -> SelectOfScalar[_TScalar_0]: # type: ignore - ... - - -# Generated overloads start - - -@overload -def select( # type: ignore - __ent0: _TCCA[_T0], - __ent1: _TCCA[_T1], -) -> Select[Tuple[_T0, _T1]]: - ... - - -@overload -def select( # type: ignore - __ent0: _TCCA[_T0], - entity_1: _TScalar_1, -) -> Select[Tuple[_T0, _TScalar_1]]: - ... - - -@overload -def select( # type: ignore - entity_0: _TScalar_0, - __ent1: _TCCA[_T1], -) -> Select[Tuple[_TScalar_0, _T1]]: - ... - - -@overload -def select( # type: ignore - entity_0: _TScalar_0, - entity_1: _TScalar_1, -) -> Select[Tuple[_TScalar_0, _TScalar_1]]: - ... - - -@overload -def select( # type: ignore - __ent0: _TCCA[_T0], - __ent1: _TCCA[_T1], - __ent2: _TCCA[_T2], -) -> Select[Tuple[_T0, _T1, _T2]]: - ... - - -@overload -def select( # type: ignore - __ent0: _TCCA[_T0], - __ent1: _TCCA[_T1], - entity_2: _TScalar_2, -) -> Select[Tuple[_T0, _T1, _TScalar_2]]: - ... - - -@overload -def select( # type: ignore - __ent0: _TCCA[_T0], - entity_1: _TScalar_1, - __ent2: _TCCA[_T2], -) -> Select[Tuple[_T0, _TScalar_1, _T2]]: - ... - - -@overload -def select( # type: ignore - __ent0: _TCCA[_T0], - entity_1: _TScalar_1, - entity_2: _TScalar_2, -) -> Select[Tuple[_T0, _TScalar_1, _TScalar_2]]: - ... - - -@overload -def select( # type: ignore - entity_0: _TScalar_0, - __ent1: _TCCA[_T1], - __ent2: _TCCA[_T2], -) -> Select[Tuple[_TScalar_0, _T1, _T2]]: - ... - - -@overload -def select( # type: ignore - entity_0: _TScalar_0, - __ent1: _TCCA[_T1], - entity_2: _TScalar_2, -) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2]]: - ... - - -@overload -def select( # type: ignore - entity_0: _TScalar_0, - entity_1: _TScalar_1, - __ent2: _TCCA[_T2], -) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2]]: - ... - - -@overload -def select( # type: ignore - entity_0: _TScalar_0, - entity_1: _TScalar_1, - entity_2: _TScalar_2, -) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2]]: - ... - - -@overload -def select( # type: ignore - __ent0: _TCCA[_T0], - __ent1: _TCCA[_T1], - __ent2: _TCCA[_T2], - __ent3: _TCCA[_T3], -) -> Select[Tuple[_T0, _T1, _T2, _T3]]: - ... - - -@overload -def select( # type: ignore - __ent0: _TCCA[_T0], - __ent1: _TCCA[_T1], - __ent2: _TCCA[_T2], - entity_3: _TScalar_3, -) -> Select[Tuple[_T0, _T1, _T2, _TScalar_3]]: - ... - - -@overload -def select( # type: ignore - __ent0: _TCCA[_T0], - __ent1: _TCCA[_T1], - entity_2: _TScalar_2, - __ent3: _TCCA[_T3], -) -> Select[Tuple[_T0, _T1, _TScalar_2, _T3]]: - ... - - -@overload -def select( # type: ignore - __ent0: _TCCA[_T0], - __ent1: _TCCA[_T1], - entity_2: _TScalar_2, - entity_3: _TScalar_3, -) -> Select[Tuple[_T0, _T1, _TScalar_2, _TScalar_3]]: - ... - - -@overload -def select( # type: ignore - __ent0: _TCCA[_T0], - entity_1: _TScalar_1, - __ent2: _TCCA[_T2], - __ent3: _TCCA[_T3], -) -> Select[Tuple[_T0, _TScalar_1, _T2, _T3]]: - ... - - -@overload -def select( # type: ignore - __ent0: _TCCA[_T0], - entity_1: _TScalar_1, - __ent2: _TCCA[_T2], - entity_3: _TScalar_3, -) -> Select[Tuple[_T0, _TScalar_1, _T2, _TScalar_3]]: - ... - - -@overload -def select( # type: ignore - __ent0: _TCCA[_T0], - entity_1: _TScalar_1, - entity_2: _TScalar_2, - __ent3: _TCCA[_T3], -) -> Select[Tuple[_T0, _TScalar_1, _TScalar_2, _T3]]: - ... - - -@overload -def select( # type: ignore - __ent0: _TCCA[_T0], - entity_1: _TScalar_1, - entity_2: _TScalar_2, - entity_3: _TScalar_3, -) -> Select[Tuple[_T0, _TScalar_1, _TScalar_2, _TScalar_3]]: - ... - - -@overload -def select( # type: ignore - entity_0: _TScalar_0, - __ent1: _TCCA[_T1], - __ent2: _TCCA[_T2], - __ent3: _TCCA[_T3], -) -> Select[Tuple[_TScalar_0, _T1, _T2, _T3]]: - ... - - -@overload -def select( # type: ignore - entity_0: _TScalar_0, - __ent1: _TCCA[_T1], - __ent2: _TCCA[_T2], - entity_3: _TScalar_3, -) -> Select[Tuple[_TScalar_0, _T1, _T2, _TScalar_3]]: - ... - - -@overload -def select( # type: ignore - entity_0: _TScalar_0, - __ent1: _TCCA[_T1], - entity_2: _TScalar_2, - __ent3: _TCCA[_T3], -) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2, _T3]]: - ... - - -@overload -def select( # type: ignore - entity_0: _TScalar_0, - __ent1: _TCCA[_T1], - entity_2: _TScalar_2, - entity_3: _TScalar_3, -) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2, _TScalar_3]]: - ... - - -@overload -def select( # type: ignore - entity_0: _TScalar_0, - entity_1: _TScalar_1, - __ent2: _TCCA[_T2], - __ent3: _TCCA[_T3], -) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2, _T3]]: - ... - - -@overload -def select( # type: ignore - entity_0: _TScalar_0, - entity_1: _TScalar_1, - __ent2: _TCCA[_T2], - entity_3: _TScalar_3, -) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2, _TScalar_3]]: - ... - - -@overload -def select( # type: ignore - entity_0: _TScalar_0, - entity_1: _TScalar_1, - entity_2: _TScalar_2, - __ent3: _TCCA[_T3], -) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2, _T3]]: - ... - - -@overload -def select( # type: ignore - entity_0: _TScalar_0, - entity_1: _TScalar_1, - entity_2: _TScalar_2, - entity_3: _TScalar_3, -) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2, _TScalar_3]]: - ... - - -# Generated overloads end - - -def select(*entities: Any) -> Union[Select, SelectOfScalar]: # type: ignore - if len(entities) == 1: - return SelectOfScalar(*entities) - return Select(*entities) + return sqlalchemy.within_group(element, *order_by) def col(column_expression: _T) -> Mapped[_T]: diff --git a/sqlmodel/sql/expression.py.jinja2 b/sqlmodel/sql/expression.py.jinja2 deleted file mode 100644 index 53babe1bbe..0000000000 --- a/sqlmodel/sql/expression.py.jinja2 +++ /dev/null @@ -1,309 +0,0 @@ -from datetime import datetime -from typing import ( - Any, - Iterable, - Mapping, - Optional, - Sequence, - Tuple, - Type, - TypeVar, - Union, - overload, -) -from uuid import UUID - -import sqlalchemy -from sqlalchemy import ( - Column, - ColumnElement, - Extract, - FunctionElement, - FunctionFilter, - Label, - Over, - TypeCoerce, - WithinGroup, -) -from sqlalchemy.orm import InstrumentedAttribute, Mapped -from sqlalchemy.sql._typing import ( - _ColumnExpressionArgument, - _ColumnExpressionOrLiteralArgument, - _ColumnExpressionOrStrLabelArgument, -) -from sqlalchemy.sql.elements import ( - BinaryExpression, - Case, - Cast, - CollectionAggregate, - ColumnClause, - SQLCoreOperations, - TryCast, - UnaryExpression, -) -from sqlalchemy.sql.expression import Select as _Select -from sqlalchemy.sql.roles import TypedColumnsClauseRole -from sqlalchemy.sql.type_api import TypeEngine -from typing_extensions import Literal, Self - -_T = TypeVar("_T") - -_TypeEngineArgument = Union[Type[TypeEngine[_T]], TypeEngine[_T]] - -# Redefine operatos that would only take a column expresion to also take the (virtual) -# types of Pydantic models, e.g. str instead of only Mapped[str]. - - -def all_(expr: Union[_ColumnExpressionArgument[_T], _T]) -> CollectionAggregate[bool]: - return sqlalchemy.all_(expr) # type: ignore[arg-type] - - -def and_( - initial_clause: Union[Literal[True], _ColumnExpressionArgument[bool], bool], - *clauses: Union[_ColumnExpressionArgument[bool], bool], -) -> ColumnElement[bool]: - return sqlalchemy.and_(initial_clause, *clauses) # type: ignore[arg-type] - - -def any_(expr: Union[_ColumnExpressionArgument[_T], _T]) -> CollectionAggregate[bool]: - return sqlalchemy.any_(expr) # type: ignore[arg-type] - - -def asc( - column: Union[_ColumnExpressionOrStrLabelArgument[_T], _T], -) -> UnaryExpression[_T]: - return sqlalchemy.asc(column) # type: ignore[arg-type] - - -def collate( - expression: Union[_ColumnExpressionArgument[str], str], collation: str -) -> BinaryExpression[str]: - return sqlalchemy.collate(expression, collation) # type: ignore[arg-type] - - -def between( - expr: Union[_ColumnExpressionOrLiteralArgument[_T], _T], - lower_bound: Any, - upper_bound: Any, - symmetric: bool = False, -) -> BinaryExpression[bool]: - return sqlalchemy.between(expr, lower_bound, upper_bound, symmetric=symmetric) # type: ignore[arg-type] - - -def not_(clause: Union[_ColumnExpressionArgument[_T], _T]) -> ColumnElement[_T]: - return sqlalchemy.not_(clause) # type: ignore[arg-type] - - -def case( - *whens: Union[ - Tuple[Union[_ColumnExpressionArgument[bool], bool], Any], Mapping[Any, Any] - ], - value: Optional[Any] = None, - else_: Optional[Any] = None, -) -> Case[Any]: - return sqlalchemy.case(*whens, value=value, else_=else_) # type: ignore[arg-type] - - -def cast( - expression: Union[_ColumnExpressionOrLiteralArgument[Any], Any], - type_: "_TypeEngineArgument[_T]", -) -> Cast[_T]: - return sqlalchemy.cast(expression, type_) # type: ignore[arg-type] - - -def try_cast( - expression: Union[_ColumnExpressionOrLiteralArgument[Any], Any], - type_: "_TypeEngineArgument[_T]", -) -> TryCast[_T]: - return sqlalchemy.try_cast(expression, type_) # type: ignore[arg-type] - - -def desc( - column: Union[_ColumnExpressionOrStrLabelArgument[_T], _T], -) -> UnaryExpression[_T]: - return sqlalchemy.desc(column) # type: ignore[arg-type] - - -def distinct(expr: Union[_ColumnExpressionArgument[_T], _T]) -> UnaryExpression[_T]: - return sqlalchemy.distinct(expr) # type: ignore[arg-type] - - -def bitwise_not(expr: Union[_ColumnExpressionArgument[_T], _T]) -> UnaryExpression[_T]: - return sqlalchemy.bitwise_not(expr) # type: ignore[arg-type] - - -def extract(field: str, expr: Union[_ColumnExpressionArgument[Any], Any]) -> Extract: - return sqlalchemy.extract(field, expr) # type: ignore[arg-type] - - -def funcfilter( - func: FunctionElement[_T], *criterion: Union[_ColumnExpressionArgument[bool], bool] -) -> FunctionFilter[_T]: - return sqlalchemy.funcfilter(func, *criterion) # type: ignore[arg-type] - - -def label( - name: str, - element: Union[_ColumnExpressionArgument[_T], _T], - type_: Optional["_TypeEngineArgument[_T]"] = None, -) -> Label[_T]: - return sqlalchemy.label(name, element, type_=type_) # type: ignore[arg-type] - - -def nulls_first( - column: Union[_ColumnExpressionArgument[_T], _T], -) -> UnaryExpression[_T]: - return sqlalchemy.nulls_first(column) # type: ignore[arg-type] - - -def nulls_last(column: Union[_ColumnExpressionArgument[_T], _T]) -> UnaryExpression[_T]: - return sqlalchemy.nulls_last(column) # type: ignore[arg-type] - - -def or_( # type: ignore[empty-body] - initial_clause: Union[Literal[False], _ColumnExpressionArgument[bool], bool], - *clauses: Union[_ColumnExpressionArgument[bool], bool], -) -> ColumnElement[bool]: - return sqlalchemy.or_(initial_clause, *clauses) # type: ignore[arg-type] - - -def over( - element: FunctionElement[_T], - partition_by: Optional[ - Union[ - Iterable[Union[_ColumnExpressionArgument[Any], Any]], - _ColumnExpressionArgument[Any], - Any, - ] - ] = None, - order_by: Optional[ - Union[ - Iterable[Union[_ColumnExpressionArgument[Any], Any]], - _ColumnExpressionArgument[Any], - Any, - ] - ] = None, - range_: Optional[Tuple[Optional[int], Optional[int]]] = None, - rows: Optional[Tuple[Optional[int], Optional[int]]] = None, -) -> Over[_T]: - return sqlalchemy.over( - element, partition_by=partition_by, order_by=order_by, range_=range_, rows=rows - ) # type: ignore[arg-type] - - -def tuple_( - *clauses: Union[_ColumnExpressionArgument[Any], Any], - types: Optional[Sequence["_TypeEngineArgument[Any]"]] = None, -) -> Tuple[Any, ...]: - return sqlalchemy.tuple_(*clauses, types=types) # type: ignore[return-value] - - -def type_coerce( - expression: Union[_ColumnExpressionOrLiteralArgument[Any], Any], - type_: "_TypeEngineArgument[_T]", -) -> TypeCoerce[_T]: - return sqlalchemy.type_coerce(expression, type_) # type: ignore[arg-type] - - -def within_group( - element: FunctionElement[_T], *order_by: Union[_ColumnExpressionArgument[Any], Any] -) -> WithinGroup[_T]: - return sqlalchemy.within_group(element, *order_by) # type: ignore[arg-type] - - -# Separate this class in SelectBase, Select, and SelectOfScalar so that they can share -# where and having without having type overlap incompatibility in session.exec(). -class SelectBase(_Select[Tuple[_T]]): - inherit_cache = True - - def where(self, *whereclause: Union[_ColumnExpressionArgument[bool], bool]) -> Self: - """Return a new `Select` construct with the given expression added to - its `WHERE` clause, joined to the existing clause via `AND`, if any. - """ - return super().where(*whereclause) # type: ignore[arg-type] - - def having(self, *having: Union[_ColumnExpressionArgument[bool], bool]) -> Self: - """Return a new `Select` construct with the given expression added to - its `HAVING` clause, joined to the existing clause via `AND`, if any. - """ - return super().having(*having) # type: ignore[arg-type] - - -class Select(SelectBase[_T]): - inherit_cache = True - - -# This is not comparable to sqlalchemy.sql.selectable.ScalarSelect, that has a different -# purpose. This is the same as a normal SQLAlchemy Select class where there's only one -# entity, so the result will be converted to a scalar by default. This way writing -# for loops on the results will feel natural. -class SelectOfScalar(SelectBase[_T]): - inherit_cache = True - - -_TCCA = Union[ - TypedColumnsClauseRole[_T], - SQLCoreOperations[_T], - Type[_T], -] - -# Generated TypeVars start - - -{% for i in range(number_of_types) %} -_TScalar_{{ i }} = TypeVar( - "_TScalar_{{ i }}", - Column, # type: ignore - Sequence, # type: ignore - Mapping, # type: ignore - UUID, - datetime, - float, - int, - bool, - bytes, - str, - None, -) - -_T{{ i }} = TypeVar("_T{{ i }}") - -{% endfor %} - -# Generated TypeVars end - -@overload -def select(__ent0: _TCCA[_T0]) -> SelectOfScalar[_T0]: - ... - - -@overload -def select(__ent0: _TScalar_0) -> SelectOfScalar[_TScalar_0]: # type: ignore - ... - - -# Generated overloads start - -{% for signature in signatures %} - -@overload -def select( # type: ignore - {% for arg in signature[0] %}{{ arg.name }}: {{ arg.annotation }}, {% endfor %} - ) -> Select[Tuple[{%for ret in signature[1] %}{{ ret }} {% if not loop.last %}, {% endif %}{% endfor %}]]: - ... - -{% endfor %} - -# Generated overloads end - - -def select(*entities: Any) -> Union[Select, SelectOfScalar]: # type: ignore - if len(entities) == 1: - return SelectOfScalar(*entities) - return Select(*entities) - - -def col(column_expression: _T) -> Mapped[_T]: - if not isinstance(column_expression, (ColumnClause, Column, InstrumentedAttribute)): - raise RuntimeError(f"Not a SQLAlchemy column: {column_expression}") - return column_expression # type: ignore From 9f3af8507ebeb045dbfb63656c0a277f4d65761a Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 4 Jun 2024 02:35:16 +0000 Subject: [PATCH 379/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 9d9d6f5fff..2d35084ba3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,6 +6,10 @@ * 🐛 Fix set varchar limit when `max_length` is set on Pydantic models using Pydantic v2. PR [#963](https://github.com/tiangolo/sqlmodel/pull/963) by [@estebanx64](https://github.com/estebanx64). +### Refactors + +* ♻️ Refactor generate select template to isolate templated code to the minimum. PR [#967](https://github.com/tiangolo/sqlmodel/pull/967) by [@tiangolo](https://github.com/tiangolo). + ### Upgrades * ⬆️ Update minimum SQLAlchemy version to 2.0.14 as that one includes `TryCast` used internally. PR [#964](https://github.com/tiangolo/sqlmodel/pull/964) by [@tiangolo](https://github.com/tiangolo). From 1d43bd8b1e26b14b541f77feda0f606916df9699 Mon Sep 17 00:00:00 2001 From: Esteban Maya Date: Mon, 3 Jun 2024 21:47:40 -0500 Subject: [PATCH 380/906] =?UTF-8?q?=F0=9F=90=9B=20Fix=20pydantic=20`EmailS?= =?UTF-8?q?tr`=20support=20and=20`max=5Flength`=20in=20several=20String=20?= =?UTF-8?q?subclasses=20(#966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/main.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index a16428b192..40051a522c 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -25,7 +25,7 @@ overload, ) -from pydantic import BaseModel +from pydantic import BaseModel, EmailStr from pydantic.fields import FieldInfo as PydanticFieldInfo from sqlalchemy import ( Boolean, @@ -574,7 +574,18 @@ def get_sqlalchemy_type(field: Any) -> Any: # Check enums first as an enum can also be a str, needed by Pydantic/FastAPI if issubclass(type_, Enum): return sa_Enum(type_) - if issubclass(type_, str): + if issubclass( + type_, + ( + str, + ipaddress.IPv4Address, + ipaddress.IPv4Network, + ipaddress.IPv6Address, + ipaddress.IPv6Network, + Path, + EmailStr, + ), + ): max_length = getattr(metadata, "max_length", None) if max_length: return AutoString(length=max_length) @@ -600,16 +611,6 @@ def get_sqlalchemy_type(field: Any) -> Any: precision=getattr(metadata, "max_digits", None), scale=getattr(metadata, "decimal_places", None), ) - if issubclass(type_, ipaddress.IPv4Address): - return AutoString - if issubclass(type_, ipaddress.IPv4Network): - return AutoString - if issubclass(type_, ipaddress.IPv6Address): - return AutoString - if issubclass(type_, ipaddress.IPv6Network): - return AutoString - if issubclass(type_, Path): - return AutoString if issubclass(type_, uuid.UUID): return GUID raise ValueError(f"{type_} has no matching SQLAlchemy type") From ceac7bc2e81f4751b1b4d1889e2be6057e25ea8f Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 4 Jun 2024 02:48:00 +0000 Subject: [PATCH 381/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 2d35084ba3..8dc4d9e62c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Fixes +* 🐛 Fix pydantic `EmailStr` support and `max_length` in several String subclasses. PR [#966](https://github.com/tiangolo/sqlmodel/pull/966) by [@estebanx64](https://github.com/estebanx64). * 🐛 Fix set varchar limit when `max_length` is set on Pydantic models using Pydantic v2. PR [#963](https://github.com/tiangolo/sqlmodel/pull/963) by [@estebanx64](https://github.com/estebanx64). ### Refactors From b93dd9512543366c078b131970aef1b3d68a9db5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Jun 2024 22:18:48 -0500 Subject: [PATCH 382/906] =?UTF-8?q?=E2=AC=86=20Bump=20tiangolo/issue-manag?= =?UTF-8?q?er=20from=200.4.1=20to=200.5.0=20(#922)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [tiangolo/issue-manager](https://github.com/tiangolo/issue-manager) from 0.4.1 to 0.5.0. - [Release notes](https://github.com/tiangolo/issue-manager/releases) - [Commits](https://github.com/tiangolo/issue-manager/compare/0.4.1...0.5.0) --- updated-dependencies: - dependency-name: tiangolo/issue-manager dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/issue-manager.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml index 4eeda34b45..68f1391f25 100644 --- a/.github/workflows/issue-manager.yml +++ b/.github/workflows/issue-manager.yml @@ -18,7 +18,7 @@ jobs: issue-manager: runs-on: ubuntu-latest steps: - - uses: tiangolo/issue-manager@0.4.1 + - uses: tiangolo/issue-manager@0.5.0 with: token: ${{ secrets.GITHUB_TOKEN }} config: > From e2f646dea506233e9f88e122e19bbd7feeb044ed Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 4 Jun 2024 03:19:04 +0000 Subject: [PATCH 383/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 8dc4d9e62c..1acd807efe 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -21,6 +21,7 @@ ### Internal +* ⬆ Bump tiangolo/issue-manager from 0.4.1 to 0.5.0. PR [#922](https://github.com/tiangolo/sqlmodel/pull/922) by [@dependabot[bot]](https://github.com/apps/dependabot). * 📌 Pin typing-extensions in tests for compatiblity with Python 3.8, dirty-equals, Pydantic. PR [#965](https://github.com/tiangolo/sqlmodel/pull/965) by [@tiangolo](https://github.com/tiangolo). * 👷 Update GitHub Actions to download and upload artifacts. PR [#936](https://github.com/tiangolo/sqlmodel/pull/936) by [@tiangolo](https://github.com/tiangolo). * 👷 Tweak CI for test-redistribute, add needed env vars for slim. PR [#929](https://github.com/tiangolo/sqlmodel/pull/929) by [@tiangolo](https://github.com/tiangolo). From b560e9deb8d5ecb9982802e479faf3bedcc7a4b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 3 Jun 2024 22:22:04 -0500 Subject: [PATCH 384/906] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20Ruff=20a?= =?UTF-8?q?nd=20Black=20(#968)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- requirements-docs-tests.txt | 2 +- requirements-docs.txt | 4 +- requirements-tests.txt | 2 +- scripts/test.sh | 1 - sqlmodel/ext/asyncio/session.py | 6 +- sqlmodel/main.py | 12 +-- sqlmodel/orm/session.py | 6 +- sqlmodel/sql/_expression_select_gen.py | 87 +++++++------------ sqlmodel/sql/_expression_select_gen.py.jinja2 | 6 +- tests/test_select_gen.py | 19 ++++ 11 files changed, 63 insertions(+), 84 deletions(-) create mode 100644 tests/test_select_gen.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3289dd0950..6b2ad2b895 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.2.0 + rev: v0.4.7 hooks: - id: ruff args: diff --git a/requirements-docs-tests.txt b/requirements-docs-tests.txt index 28f1ad1be9..c65317a7cf 100644 --- a/requirements-docs-tests.txt +++ b/requirements-docs-tests.txt @@ -1,2 +1,2 @@ # For mkdocstrings and code generator using templates -black >=22.10,<24.0 +black >=22.10 diff --git a/requirements-docs.txt b/requirements-docs.txt index cacb5dc2a3..d3a1838af8 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -12,7 +12,7 @@ pillow==10.1.0 # For image processing by Material for MkDocs cairosvg==2.7.0 mkdocstrings[python]==0.23.0 -griffe-typingdoc==0.2.2 +# Enable griffe-typingdoc once dropping Python 3.7 and upgrading typing-extensions +# griffe-typingdoc==0.2.5 # For griffe, it formats with black -black==23.3.0 typer == 0.12.3 diff --git a/requirements-tests.txt b/requirements-tests.txt index 3c2578e9c5..8801bb9b91 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -3,7 +3,7 @@ pytest >=7.0.1,<8.0.0 coverage[toml] >=6.2,<8.0 mypy ==1.4.1 -ruff ==0.2.0 +ruff ==0.4.7 # For FastAPI tests fastapi >=0.103.2 httpx ==0.24.1 diff --git a/scripts/test.sh b/scripts/test.sh index 1460a9c7ec..9b758bdbdf 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -3,7 +3,6 @@ set -e set -x -CHECK_JINJA=1 python scripts/generate_select.py coverage run -m pytest tests coverage combine coverage report --show-missing diff --git a/sqlmodel/ext/asyncio/session.py b/sqlmodel/ext/asyncio/session.py index 012d8ef5e4..467d0bd84e 100644 --- a/sqlmodel/ext/asyncio/session.py +++ b/sqlmodel/ext/asyncio/session.py @@ -43,8 +43,7 @@ async def exec( bind_arguments: Optional[Dict[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, - ) -> TupleResult[_TSelectParam]: - ... + ) -> TupleResult[_TSelectParam]: ... @overload async def exec( @@ -56,8 +55,7 @@ async def exec( bind_arguments: Optional[Dict[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, - ) -> ScalarResult[_TSelectParam]: - ... + ) -> ScalarResult[_TSelectParam]: ... async def exec( self, diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 40051a522c..505683f756 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -231,8 +231,7 @@ def Field( sa_column_args: Union[Sequence[Any], UndefinedType] = Undefined, sa_column_kwargs: Union[Mapping[str, Any], UndefinedType] = Undefined, schema_extra: Optional[Dict[str, Any]] = None, -) -> Any: - ... +) -> Any: ... @overload @@ -268,8 +267,7 @@ def Field( repr: bool = True, sa_column: Union[Column, UndefinedType] = Undefined, # type: ignore schema_extra: Optional[Dict[str, Any]] = None, -) -> Any: - ... +) -> Any: ... def Field( @@ -361,8 +359,7 @@ def Relationship( link_model: Optional[Any] = None, sa_relationship_args: Optional[Sequence[Any]] = None, sa_relationship_kwargs: Optional[Mapping[str, Any]] = None, -) -> Any: - ... +) -> Any: ... @overload @@ -371,8 +368,7 @@ def Relationship( back_populates: Optional[str] = None, link_model: Optional[Any] = None, sa_relationship: Optional[RelationshipProperty[Any]] = None, -) -> Any: - ... +) -> Any: ... def Relationship( diff --git a/sqlmodel/orm/session.py b/sqlmodel/orm/session.py index e404bb137d..b60875095b 100644 --- a/sqlmodel/orm/session.py +++ b/sqlmodel/orm/session.py @@ -35,8 +35,7 @@ def exec( bind_arguments: Optional[Dict[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, - ) -> TupleResult[_TSelectParam]: - ... + ) -> TupleResult[_TSelectParam]: ... @overload def exec( @@ -48,8 +47,7 @@ def exec( bind_arguments: Optional[Dict[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, - ) -> ScalarResult[_TSelectParam]: - ... + ) -> ScalarResult[_TSelectParam]: ... def exec( self, diff --git a/sqlmodel/sql/_expression_select_gen.py b/sqlmodel/sql/_expression_select_gen.py index b6c15742fa..08aa59ad61 100644 --- a/sqlmodel/sql/_expression_select_gen.py +++ b/sqlmodel/sql/_expression_select_gen.py @@ -111,8 +111,7 @@ @overload -def select(__ent0: _TCCA[_T0]) -> SelectOfScalar[_T0]: - ... +def select(__ent0: _TCCA[_T0]) -> SelectOfScalar[_T0]: ... @overload @@ -127,32 +126,28 @@ def select(__ent0: _TScalar_0) -> SelectOfScalar[_TScalar_0]: # type: ignore def select( # type: ignore __ent0: _TCCA[_T0], __ent1: _TCCA[_T1], -) -> Select[Tuple[_T0, _T1]]: - ... +) -> Select[Tuple[_T0, _T1]]: ... @overload def select( # type: ignore __ent0: _TCCA[_T0], entity_1: _TScalar_1, -) -> Select[Tuple[_T0, _TScalar_1]]: - ... +) -> Select[Tuple[_T0, _TScalar_1]]: ... @overload def select( # type: ignore entity_0: _TScalar_0, __ent1: _TCCA[_T1], -) -> Select[Tuple[_TScalar_0, _T1]]: - ... +) -> Select[Tuple[_TScalar_0, _T1]]: ... @overload def select( # type: ignore entity_0: _TScalar_0, entity_1: _TScalar_1, -) -> Select[Tuple[_TScalar_0, _TScalar_1]]: - ... +) -> Select[Tuple[_TScalar_0, _TScalar_1]]: ... @overload @@ -160,8 +155,7 @@ def select( # type: ignore __ent0: _TCCA[_T0], __ent1: _TCCA[_T1], __ent2: _TCCA[_T2], -) -> Select[Tuple[_T0, _T1, _T2]]: - ... +) -> Select[Tuple[_T0, _T1, _T2]]: ... @overload @@ -169,8 +163,7 @@ def select( # type: ignore __ent0: _TCCA[_T0], __ent1: _TCCA[_T1], entity_2: _TScalar_2, -) -> Select[Tuple[_T0, _T1, _TScalar_2]]: - ... +) -> Select[Tuple[_T0, _T1, _TScalar_2]]: ... @overload @@ -178,8 +171,7 @@ def select( # type: ignore __ent0: _TCCA[_T0], entity_1: _TScalar_1, __ent2: _TCCA[_T2], -) -> Select[Tuple[_T0, _TScalar_1, _T2]]: - ... +) -> Select[Tuple[_T0, _TScalar_1, _T2]]: ... @overload @@ -187,8 +179,7 @@ def select( # type: ignore __ent0: _TCCA[_T0], entity_1: _TScalar_1, entity_2: _TScalar_2, -) -> Select[Tuple[_T0, _TScalar_1, _TScalar_2]]: - ... +) -> Select[Tuple[_T0, _TScalar_1, _TScalar_2]]: ... @overload @@ -196,8 +187,7 @@ def select( # type: ignore entity_0: _TScalar_0, __ent1: _TCCA[_T1], __ent2: _TCCA[_T2], -) -> Select[Tuple[_TScalar_0, _T1, _T2]]: - ... +) -> Select[Tuple[_TScalar_0, _T1, _T2]]: ... @overload @@ -205,8 +195,7 @@ def select( # type: ignore entity_0: _TScalar_0, __ent1: _TCCA[_T1], entity_2: _TScalar_2, -) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2]]: - ... +) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2]]: ... @overload @@ -214,8 +203,7 @@ def select( # type: ignore entity_0: _TScalar_0, entity_1: _TScalar_1, __ent2: _TCCA[_T2], -) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2]]: - ... +) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2]]: ... @overload @@ -223,8 +211,7 @@ def select( # type: ignore entity_0: _TScalar_0, entity_1: _TScalar_1, entity_2: _TScalar_2, -) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2]]: - ... +) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2]]: ... @overload @@ -233,8 +220,7 @@ def select( # type: ignore __ent1: _TCCA[_T1], __ent2: _TCCA[_T2], __ent3: _TCCA[_T3], -) -> Select[Tuple[_T0, _T1, _T2, _T3]]: - ... +) -> Select[Tuple[_T0, _T1, _T2, _T3]]: ... @overload @@ -243,8 +229,7 @@ def select( # type: ignore __ent1: _TCCA[_T1], __ent2: _TCCA[_T2], entity_3: _TScalar_3, -) -> Select[Tuple[_T0, _T1, _T2, _TScalar_3]]: - ... +) -> Select[Tuple[_T0, _T1, _T2, _TScalar_3]]: ... @overload @@ -253,8 +238,7 @@ def select( # type: ignore __ent1: _TCCA[_T1], entity_2: _TScalar_2, __ent3: _TCCA[_T3], -) -> Select[Tuple[_T0, _T1, _TScalar_2, _T3]]: - ... +) -> Select[Tuple[_T0, _T1, _TScalar_2, _T3]]: ... @overload @@ -263,8 +247,7 @@ def select( # type: ignore __ent1: _TCCA[_T1], entity_2: _TScalar_2, entity_3: _TScalar_3, -) -> Select[Tuple[_T0, _T1, _TScalar_2, _TScalar_3]]: - ... +) -> Select[Tuple[_T0, _T1, _TScalar_2, _TScalar_3]]: ... @overload @@ -273,8 +256,7 @@ def select( # type: ignore entity_1: _TScalar_1, __ent2: _TCCA[_T2], __ent3: _TCCA[_T3], -) -> Select[Tuple[_T0, _TScalar_1, _T2, _T3]]: - ... +) -> Select[Tuple[_T0, _TScalar_1, _T2, _T3]]: ... @overload @@ -283,8 +265,7 @@ def select( # type: ignore entity_1: _TScalar_1, __ent2: _TCCA[_T2], entity_3: _TScalar_3, -) -> Select[Tuple[_T0, _TScalar_1, _T2, _TScalar_3]]: - ... +) -> Select[Tuple[_T0, _TScalar_1, _T2, _TScalar_3]]: ... @overload @@ -293,8 +274,7 @@ def select( # type: ignore entity_1: _TScalar_1, entity_2: _TScalar_2, __ent3: _TCCA[_T3], -) -> Select[Tuple[_T0, _TScalar_1, _TScalar_2, _T3]]: - ... +) -> Select[Tuple[_T0, _TScalar_1, _TScalar_2, _T3]]: ... @overload @@ -303,8 +283,7 @@ def select( # type: ignore entity_1: _TScalar_1, entity_2: _TScalar_2, entity_3: _TScalar_3, -) -> Select[Tuple[_T0, _TScalar_1, _TScalar_2, _TScalar_3]]: - ... +) -> Select[Tuple[_T0, _TScalar_1, _TScalar_2, _TScalar_3]]: ... @overload @@ -313,8 +292,7 @@ def select( # type: ignore __ent1: _TCCA[_T1], __ent2: _TCCA[_T2], __ent3: _TCCA[_T3], -) -> Select[Tuple[_TScalar_0, _T1, _T2, _T3]]: - ... +) -> Select[Tuple[_TScalar_0, _T1, _T2, _T3]]: ... @overload @@ -323,8 +301,7 @@ def select( # type: ignore __ent1: _TCCA[_T1], __ent2: _TCCA[_T2], entity_3: _TScalar_3, -) -> Select[Tuple[_TScalar_0, _T1, _T2, _TScalar_3]]: - ... +) -> Select[Tuple[_TScalar_0, _T1, _T2, _TScalar_3]]: ... @overload @@ -333,8 +310,7 @@ def select( # type: ignore __ent1: _TCCA[_T1], entity_2: _TScalar_2, __ent3: _TCCA[_T3], -) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2, _T3]]: - ... +) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2, _T3]]: ... @overload @@ -343,8 +319,7 @@ def select( # type: ignore __ent1: _TCCA[_T1], entity_2: _TScalar_2, entity_3: _TScalar_3, -) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2, _TScalar_3]]: - ... +) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2, _TScalar_3]]: ... @overload @@ -353,8 +328,7 @@ def select( # type: ignore entity_1: _TScalar_1, __ent2: _TCCA[_T2], __ent3: _TCCA[_T3], -) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2, _T3]]: - ... +) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2, _T3]]: ... @overload @@ -363,8 +337,7 @@ def select( # type: ignore entity_1: _TScalar_1, __ent2: _TCCA[_T2], entity_3: _TScalar_3, -) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2, _TScalar_3]]: - ... +) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2, _TScalar_3]]: ... @overload @@ -373,8 +346,7 @@ def select( # type: ignore entity_1: _TScalar_1, entity_2: _TScalar_2, __ent3: _TCCA[_T3], -) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2, _T3]]: - ... +) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2, _T3]]: ... @overload @@ -383,8 +355,7 @@ def select( # type: ignore entity_1: _TScalar_1, entity_2: _TScalar_2, entity_3: _TScalar_3, -) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2, _TScalar_3]]: - ... +) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2, _TScalar_3]]: ... # Generated overloads end diff --git a/sqlmodel/sql/_expression_select_gen.py.jinja2 b/sqlmodel/sql/_expression_select_gen.py.jinja2 index 307e32b784..ef838e4168 100644 --- a/sqlmodel/sql/_expression_select_gen.py.jinja2 +++ b/sqlmodel/sql/_expression_select_gen.py.jinja2 @@ -56,8 +56,7 @@ _T{{ i }} = TypeVar("_T{{ i }}") # Generated TypeVars end @overload -def select(__ent0: _TCCA[_T0]) -> SelectOfScalar[_T0]: - ... +def select(__ent0: _TCCA[_T0]) -> SelectOfScalar[_T0]: ... @overload @@ -72,8 +71,7 @@ def select(__ent0: _TScalar_0) -> SelectOfScalar[_TScalar_0]: # type: ignore @overload def select( # type: ignore {% for arg in signature[0] %}{{ arg.name }}: {{ arg.annotation }}, {% endfor %} - ) -> Select[Tuple[{%for ret in signature[1] %}{{ ret }} {% if not loop.last %}, {% endif %}{% endfor %}]]: - ... + ) -> Select[Tuple[{%for ret in signature[1] %}{{ ret }} {% if not loop.last %}, {% endif %}{% endfor %}]]: ... {% endfor %} diff --git a/tests/test_select_gen.py b/tests/test_select_gen.py new file mode 100644 index 0000000000..6d578f7708 --- /dev/null +++ b/tests/test_select_gen.py @@ -0,0 +1,19 @@ +import subprocess +import sys +from pathlib import Path + +from .conftest import needs_py39 + +root_path = Path(__file__).parent.parent + + +@needs_py39 +def test_select_gen() -> None: + result = subprocess.run( + [sys.executable, "scripts/generate_select.py"], + env={"CHECK_JINJA": "1"}, + check=True, + cwd=root_path, + capture_output=True, + ) + print(result.stdout) From 883cbe3a8dc7da3ab44d57cbec7828cf93d876a7 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 4 Jun 2024 03:22:28 +0000 Subject: [PATCH 385/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 1acd807efe..d479b9b20f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -21,6 +21,7 @@ ### Internal +* ⬆️ Upgrade Ruff and Black. PR [#968](https://github.com/tiangolo/sqlmodel/pull/968) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump tiangolo/issue-manager from 0.4.1 to 0.5.0. PR [#922](https://github.com/tiangolo/sqlmodel/pull/922) by [@dependabot[bot]](https://github.com/apps/dependabot). * 📌 Pin typing-extensions in tests for compatiblity with Python 3.8, dirty-equals, Pydantic. PR [#965](https://github.com/tiangolo/sqlmodel/pull/965) by [@tiangolo](https://github.com/tiangolo). * 👷 Update GitHub Actions to download and upload artifacts. PR [#936](https://github.com/tiangolo/sqlmodel/pull/936) by [@tiangolo](https://github.com/tiangolo). From 4590963e884e7f112a2fd28a6c3396b6570dc58d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 3 Jun 2024 22:26:53 -0500 Subject: [PATCH 386/906] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.0.?= =?UTF-8?q?19?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 2 ++ sqlmodel/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index d479b9b20f..57e6a0f652 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest Changes +## 0.0.19 + ### Fixes * 🐛 Fix pydantic `EmailStr` support and `max_length` in several String subclasses. PR [#966](https://github.com/tiangolo/sqlmodel/pull/966) by [@estebanx64](https://github.com/estebanx64). diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index 397c07f5d2..61ae35f7eb 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.0.18" +__version__ = "0.0.19" # Re-export from SQLAlchemy from sqlalchemy.engine import create_engine as create_engine From f6ad19b1a7f0e23084ffd53091b0aad8df08add8 Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Tue, 4 Jun 2024 18:48:02 -0500 Subject: [PATCH 387/906] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Update=20pip=20ins?= =?UTF-8?q?tallation=20command=20in=20tutorial=20(#975)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tutorial/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/index.md b/docs/tutorial/index.md index 9b0939b0c1..88d952a746 100644 --- a/docs/tutorial/index.md +++ b/docs/tutorial/index.md @@ -192,7 +192,7 @@ Now, after making sure we are inside of a virtual environment in some way, we ca
```console -# (env) $$ python -m pip install sqlmodel +# (env) $$ pip install sqlmodel ---> 100% Successfully installed sqlmodel pydantic sqlalchemy ``` From 8703539bf0753d1411a4f6abdaa41c058161bd28 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 4 Jun 2024 23:48:20 +0000 Subject: [PATCH 388/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 57e6a0f652..e545245592 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Docs + +* ✏️ Update pip installation command in tutorial. PR [#975](https://github.com/tiangolo/sqlmodel/pull/975) by [@alejsdev](https://github.com/alejsdev). + ## 0.0.19 ### Fixes From e7c62fc9d9c1ab3287c6ee06c742f96ed299a7f0 Mon Sep 17 00:00:00 2001 From: Anderson T Date: Tue, 4 Jun 2024 16:56:52 -0700 Subject: [PATCH 389/906] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo=20in=20?= =?UTF-8?q?`sqlmodel/=5Fcompat.py`=20(#950)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index d42a62429e..4018d1bb39 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -194,7 +194,7 @@ def get_type_from_field(field: Any) -> Any: # Non optional unions are not allowed if bases[0] is not NoneType and bases[1] is not NoneType: raise ValueError( - "Cannot have a (non-optional) union as a SQLlchemy field" + "Cannot have a (non-optional) union as a SQLAlchemy field" ) # Optional unions are allowed return bases[0] if bases[0] is not NoneType else bases[1] From 6e7e5539637bb968c517a15ae83a233e618afd17 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 4 Jun 2024 23:57:10 +0000 Subject: [PATCH 390/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index e545245592..a331c22e31 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Docs +* ✏️ Fix typo in `sqlmodel/_compat.py`. PR [#950](https://github.com/tiangolo/sqlmodel/pull/950) by [@Highfire1](https://github.com/Highfire1). * ✏️ Update pip installation command in tutorial. PR [#975](https://github.com/tiangolo/sqlmodel/pull/975) by [@alejsdev](https://github.com/alejsdev). ## 0.0.19 From 24e76c7a1386e00a9aafa155fa58e310c56ef560 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mieszko=20Ba=C5=84czerowski?= Date: Wed, 5 Jun 2024 01:58:27 +0200 Subject: [PATCH 391/906] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo=20in=20?= =?UTF-8?q?`docs/tutorial/relationship-attributes/index.md`=20(#880)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tutorial/relationship-attributes/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/relationship-attributes/index.md b/docs/tutorial/relationship-attributes/index.md index b32fb637df..f63b2669e3 100644 --- a/docs/tutorial/relationship-attributes/index.md +++ b/docs/tutorial/relationship-attributes/index.md @@ -4,7 +4,7 @@ In the previous chapters we discussed how to manage databases with tables that h And then we read the data together with `select()` and using `.where()` or `.join()` to connect it. -Now we will see how to use **Relationship Attributes**, an extra feature of **SQLModel** (and SQLAlchemy) to work with the data in the database in way much more familiar way, and closer to normal Python code. +Now we will see how to use **Relationship Attributes**, an extra feature of **SQLModel** (and SQLAlchemy), to work with the data in the database in a much more familiar way, and closer to normal Python code. /// info From 23869cab0d2e50f4abab8dce228180fe7647083e Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 4 Jun 2024 23:58:51 +0000 Subject: [PATCH 392/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index a331c22e31..82d77a97f8 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest Changes +* ✏️ Fix typo in `docs/tutorial/relationship-attributes/index.md`. PR [#880](https://github.com/tiangolo/sqlmodel/pull/880) by [@UncleGoogle](https://github.com/UncleGoogle). + ### Docs * ✏️ Fix typo in `sqlmodel/_compat.py`. PR [#950](https://github.com/tiangolo/sqlmodel/pull/950) by [@Highfire1](https://github.com/Highfire1). From 1263024be557e0024cb4b6f8565b53ab8359cecf Mon Sep 17 00:00:00 2001 From: Lucien O <36520137+luco17@users.noreply.github.com> Date: Wed, 5 Jun 2024 01:00:14 +0100 Subject: [PATCH 393/906] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo=20in=20?= =?UTF-8?q?`docs/tutorial`=20(#943)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tutorial/create-db-and-table.md | 4 ++-- docs/tutorial/indexes.md | 2 +- docs/tutorial/select.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/tutorial/create-db-and-table.md b/docs/tutorial/create-db-and-table.md index d87b935a1c..f508cf7cea 100644 --- a/docs/tutorial/create-db-and-table.md +++ b/docs/tutorial/create-db-and-table.md @@ -354,7 +354,7 @@ But we will talk about it later. ### Engine Database URL -Each supported database has it's own URL type. For example, for **SQLite** it is `sqlite:///` followed by the file path. For example: +Each supported database has its own URL type. For example, for **SQLite** it is `sqlite:///` followed by the file path. For example: * `sqlite:///database.db` * `sqlite:///databases/local/application.db` @@ -470,7 +470,7 @@ If you didn't know about SQLAlchemy before and are just learning **SQLModel**, y You can read a lot more about the engine in the SQLAlchemy documentation. -**SQLModel** defines it's own `create_engine()` function. It is the same as SQLAlchemy's `create_engine()`, but with the difference that it defaults to use `future=True` (which means that it uses the style of the latest SQLAlchemy, 1.4, and the future 2.0). +**SQLModel** defines its own `create_engine()` function. It is the same as SQLAlchemy's `create_engine()`, but with the difference that it defaults to use `future=True` (which means that it uses the style of the latest SQLAlchemy, 1.4, and the future 2.0). And SQLModel's version of `create_engine()` is type annotated internally, so your editor will be able to help you with autocompletion and inline errors. diff --git a/docs/tutorial/indexes.md b/docs/tutorial/indexes.md index d0854720cf..d0724f5183 100644 --- a/docs/tutorial/indexes.md +++ b/docs/tutorial/indexes.md @@ -90,7 +90,7 @@ Do you like **fancy words**? Cool! Programmers tend to like fancy words. 😅 That algorithm I showed you above is called **Binary Search**. -It's called like that because you **search** something by splitting the dictionary (or any ordered list of things) in **two** ("binary" means "two") parts. And you do that process multiple times until you find what you want. +It's called that because you **search** something by splitting the dictionary (or any ordered list of things) in **two** ("binary" means "two") parts. And you do that process multiple times until you find what you want. /// diff --git a/docs/tutorial/select.md b/docs/tutorial/select.md index 47c80a2661..be66de351e 100644 --- a/docs/tutorial/select.md +++ b/docs/tutorial/select.md @@ -713,7 +713,7 @@ In this chapter we are touching some of them. When importing from `sqlmodel` the `select()` function, you are using **SQLModel**'s version of `select`. -SQLAchemy also has it's own `select`, and SQLModel's `select` uses SQLAlchemy's `select` internally. +SQLAchemy also has its own `select`, and SQLModel's `select` uses SQLAlchemy's `select` internally. But SQLModel's version does a lot of **tricks** with type annotations to make sure you get the best **editor support** possible, no matter if you use **VS Code**, **PyCharm**, or something else. ✨ From f1bfebc9e2dc5e637c87390c0f005129050d9d6a Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 5 Jun 2024 00:00:30 +0000 Subject: [PATCH 394/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 82d77a97f8..60453f71e7 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,6 +6,7 @@ ### Docs +* ✏️ Fix typo in `docs/tutorial`. PR [#943](https://github.com/tiangolo/sqlmodel/pull/943) by [@luco17](https://github.com/luco17). * ✏️ Fix typo in `sqlmodel/_compat.py`. PR [#950](https://github.com/tiangolo/sqlmodel/pull/950) by [@Highfire1](https://github.com/Highfire1). * ✏️ Update pip installation command in tutorial. PR [#975](https://github.com/tiangolo/sqlmodel/pull/975) by [@alejsdev](https://github.com/alejsdev). From 8416508d7952a0bf43b2c321cad28372a81cf819 Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Tue, 4 Jun 2024 20:52:36 -0500 Subject: [PATCH 395/906] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Add=20missing=20st?= =?UTF-8?q?ep=20in=20`create-db-and-table-with-db-browser.md`=20(#976)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tutorial/create-db-and-table-with-db-browser.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/tutorial/create-db-and-table-with-db-browser.md b/docs/tutorial/create-db-and-table-with-db-browser.md index 72be6db297..b3a6f960ae 100644 --- a/docs/tutorial/create-db-and-table-with-db-browser.md +++ b/docs/tutorial/create-db-and-table-with-db-browser.md @@ -125,6 +125,8 @@ And delete that `./database.db` file in your project directory. And click again on New Database. +Save the file with the name `database.db` again. + This time, if you see the dialog to create a new table, just close it by clicking the Cancel button. And now, go to the tab Execute SQL. From 96bfd855f8bd1f5250385155072686ce1b6a3ff2 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 5 Jun 2024 01:52:54 +0000 Subject: [PATCH 396/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 60453f71e7..7101d4aba6 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,6 +6,7 @@ ### Docs +* ✏️ Add missing step in `create-db-and-table-with-db-browser.md`. PR [#976](https://github.com/tiangolo/sqlmodel/pull/976) by [@alejsdev](https://github.com/alejsdev). * ✏️ Fix typo in `docs/tutorial`. PR [#943](https://github.com/tiangolo/sqlmodel/pull/943) by [@luco17](https://github.com/luco17). * ✏️ Fix typo in `sqlmodel/_compat.py`. PR [#950](https://github.com/tiangolo/sqlmodel/pull/950) by [@Highfire1](https://github.com/Highfire1). * ✏️ Update pip installation command in tutorial. PR [#975](https://github.com/tiangolo/sqlmodel/pull/975) by [@alejsdev](https://github.com/alejsdev). From 600da0a25c841992ca73e7fc519ebbd986bbcc6e Mon Sep 17 00:00:00 2001 From: Toby Penner Date: Thu, 20 Jun 2024 21:16:56 -0500 Subject: [PATCH 397/906] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20internal=20l?= =?UTF-8?q?ink=20in=20`docs/tutorial/create-db-and-table.md`=20(#911)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tutorial/create-db-and-table.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/create-db-and-table.md b/docs/tutorial/create-db-and-table.md index f508cf7cea..761c979e02 100644 --- a/docs/tutorial/create-db-and-table.md +++ b/docs/tutorial/create-db-and-table.md @@ -688,7 +688,7 @@ In the example in the previous chapter we created the table using `TEXT` for som But in this output SQLAlchemy is using `VARCHAR` instead. Let's see what's going on. -Remember that [each SQL Database has some different variations in what they support?](../databases/#sql-the-language){.internal-link target=_blank} +Remember that [each SQL Database has some different variations in what they support?](../databases.md#sql-the-language){.internal-link target=_blank} This is one of the differences. Each database supports some particular **data types**, like `INTEGER` and `TEXT`. From 3b889e09f7159d018a1535c86ab4b1e6e5c2fa94 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 21 Jun 2024 02:17:19 +0000 Subject: [PATCH 398/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 7101d4aba6..8df3f80877 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,6 +6,7 @@ ### Docs +* ✏️ Fix internal link in `docs/tutorial/create-db-and-table.md`. PR [#911](https://github.com/tiangolo/sqlmodel/pull/911) by [@tfpgh](https://github.com/tfpgh). * ✏️ Add missing step in `create-db-and-table-with-db-browser.md`. PR [#976](https://github.com/tiangolo/sqlmodel/pull/976) by [@alejsdev](https://github.com/alejsdev). * ✏️ Fix typo in `docs/tutorial`. PR [#943](https://github.com/tiangolo/sqlmodel/pull/943) by [@luco17](https://github.com/luco17). * ✏️ Fix typo in `sqlmodel/_compat.py`. PR [#950](https://github.com/tiangolo/sqlmodel/pull/950) by [@Highfire1](https://github.com/Highfire1). From 95936bb5085b6ee467c4cd9051c7bfc8395c9b13 Mon Sep 17 00:00:00 2001 From: Esteban Maya Date: Tue, 16 Jul 2024 20:52:03 -0500 Subject: [PATCH 399/906] =?UTF-8?q?=E2=9C=A8=20Add=20official=20UUID=20sup?= =?UTF-8?q?port,=20docs=20and=20tests,=20internally=20using=20new=20SQLAlc?= =?UTF-8?q?hemy=202.0=20types=20(#992)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Add UUID support from sqlalchemy 2.0 update * ⚰️ Remove dead code for GUID old support * 📝 Add documentation for UUIDs * 🧪 Add test for UUIDs field definition and support * 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks * ✏️ Fix prerequisites docs for uuid * ♻️ Update UUID source examples for consistency Keep consistency with other examples, functions without parameters, and printing info that shows and explains the UUID results (and can also be tested later) * 📝 Add source examples for selecting UUIDs with session.get() * 📝 Re-structure UUID docs * Explain the concepts at the beggining before using them. * Explain how UUIDs can be used and trusted. * Explain why UUIDs could be generated on the code, and how they can be used for distributed systems. * Explain how UUIDs can prevent information leakage. * Warn about UUIDs storage size. * Explain that uuid is part of the standard library. * Explain how default_factory works. * Explain that creating an instance would generate a new UUID, before it is sent to the DB. This is included and shown in the example, the UUID is printed before saving to the DB. * Remove sections about other operations that would behave the same as other fields and don't need additional info from what was explained in previous chapters. * Add two examples to select using UUIDs, similar to the previous ones, mainly to be able to use them in the tests and ensure that it all works, even when SQLite stores the values as strings but the where() or the session.get() receive UUID values (ensure SQLAlchemy does the conversion correctly for SQLite). * Add an example terminal run of the code, with comments. * Simplify the ending to keep only the information that wasn't there before, just the "Learn More" with links. * ✅ Refactor tests with new printed code, extract and check that UUIDs are used in the right places. * ✅ Add tests for the new extra UUID examples, for session.get() * 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks * 📝 Rename variable in example for Python 3.7+ for consistency with 3.10+ (I missed that change before) --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- docs/advanced/uuid.md | 342 ++++++++++++++++++ docs_src/advanced/uuid/__init__.py | 0 docs_src/advanced/uuid/tutorial001.py | 65 ++++ docs_src/advanced/uuid/tutorial001_py310.py | 64 ++++ docs_src/advanced/uuid/tutorial002.py | 64 ++++ docs_src/advanced/uuid/tutorial002_py310.py | 63 ++++ mkdocs.yml | 1 + sqlmodel/__init__.py | 1 - sqlmodel/main.py | 6 +- sqlmodel/sql/sqltypes.py | 47 +-- tests/test_advanced/test_uuid/__init__.py | 0 .../test_uuid/test_tutorial001.py | 71 ++++ .../test_uuid/test_tutorial001_py310.py | 72 ++++ .../test_uuid/test_tutorial002.py | 71 ++++ .../test_uuid/test_tutorial002_py310.py | 72 ++++ 15 files changed, 890 insertions(+), 49 deletions(-) create mode 100644 docs/advanced/uuid.md create mode 100644 docs_src/advanced/uuid/__init__.py create mode 100644 docs_src/advanced/uuid/tutorial001.py create mode 100644 docs_src/advanced/uuid/tutorial001_py310.py create mode 100644 docs_src/advanced/uuid/tutorial002.py create mode 100644 docs_src/advanced/uuid/tutorial002_py310.py create mode 100644 tests/test_advanced/test_uuid/__init__.py create mode 100644 tests/test_advanced/test_uuid/test_tutorial001.py create mode 100644 tests/test_advanced/test_uuid/test_tutorial001_py310.py create mode 100644 tests/test_advanced/test_uuid/test_tutorial002.py create mode 100644 tests/test_advanced/test_uuid/test_tutorial002_py310.py diff --git a/docs/advanced/uuid.md b/docs/advanced/uuid.md new file mode 100644 index 0000000000..56492636f6 --- /dev/null +++ b/docs/advanced/uuid.md @@ -0,0 +1,342 @@ +# UUID (Universally Unique Identifiers) + +We have discussed some data types like `str`, `int`, etc. + +There's another data type called `UUID` (Universally Unique Identifier). + +You might have seen **UUIDs**, for example in URLs. They look something like this: + +``` +4ff2dab7-bffe-414d-88a5-1826b9fea8df +``` + +UUIDs can be particularly useful as an alternative to auto-incrementing integers for **primary keys**. + +/// info + +Official support for UUIDs was added in SQLModel version `0.0.20`. + +/// + +## About UUIDs + +UUIDs are numbers with 128 bits, that is, 16 bytes. + +They are normally seen as 32 hexadecimal characters separated by dashes. + +There are several versions of UUID, some versions include the current time in the bytes, but **UUIDs version 4** are mainly random, the way they are generated makes them virtually **unique**. + +### Distributed UUIDs + +You could generate one UUID in one computer, and someone else could generate another UUID in another computer, and it would be almost **impossible** for both UUIDs to be the **same**. + +This means that you don't have to wait for the DB to generate the ID for you, you can **generate it in code before sending it to the database**, because you can be quite certain it will be unique. + +/// note | Technical Details + +Because the number of possible UUIDs is so large (2^128), the probability of generating the same UUID version 4 (the random ones) twice is very low. + +If you had 103 trillion version 4 UUIDs stored in the database, the probability of generating a duplicated new one is one in a billion. 🤓 + +/// + +For the same reason, if you decided to migrate your database, combine it with another database and mix records, etc. you would most probably be able to **just use the same UUIDs** you had originally. + +/// warning + +There's still a chance you could have a collision, but it's very low. In most cases you could assume you wouldn't have it, but it would be good to be prepared for it. + +/// + +### UUIDs Prevent Information Leakage + +Because UUIDs version 4 are **random**, you could give these IDs to the application users or to other systems, **without exposing information** about your application. + +When using **auto-incremented integers** for primary keys, you could implicitly expose information about your system. For example, someone could create a new hero, and by getting the hero ID `20` **they would know that you have 20 heroes** in your system (or even less, if some heroes were already deleted). + +### UUID Storage + +Because UUIDs are 16 bytes, they would **consume more space** in the database than a smaller auto-incremented integer (commonly 4 bytes). + +Depending on the database you use, UUIDs could have **better or worse performance**. If you are concerned about that, you should check the documentation for the specific database. + +SQLite doesn't have a specific UUID type, so it will store the UUID as a string. Other databases like Postgres have a specific UUID type which would result in better performance and space usage than strings. + +## Models with UUIDs + +To use UUIDs as primary keys we need to import `uuid`, which is part of the Python standard library (we don't have to install anything) and use `uuid.UUID` as the **type** for the ID field. + +We also want the Python code to **generate a new UUID** when creating a new instance, so we use `default_factory`. + +The parameter `default_factory` takes a function (or in general, a "callable"). This function will be **called when creating a new instance** of the model and the value returned by the function will be used as the default value for the field. + +For the function in `default_factory` we pass `uuid.uuid4`, which is a function that generates a **new UUID version 4**. + +/// tip + +We don't call `uuid.uuid4()` ourselves in the code (we don't put the parenthesis). Instead, we pass the function itself, just `uuid.uuid4`, so that SQLModel can call it every time we create a new instance. + +/// + +This means that the UUID will be generated in the Python code, **before sending the data to the database**. + +//// tab | Python 3.10+ + +```Python hl_lines="1 7" +{!./docs_src/advanced/uuid/tutorial001_py310.py[ln:1-10]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + +```Python hl_lines="1 8" +{!./docs_src/advanced/uuid/tutorial001.py[ln:1-11]!} + +# Code below omitted 👇 +``` + +//// + +/// details | 👀 Full file preview + +//// tab | Python 3.10+ + +```Python +{!./docs_src/advanced/uuid/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + +```Python +{!./docs_src/advanced/uuid/tutorial001.py!} +``` + +//// + +/// + +Pydantic has support for `UUID` types. + +For the database, **SQLModel** internally uses SQLAlchemy's `Uuid` type. + +### Create a Record with a UUID + +When creating a `Hero` record, the `id` field will be **automatically populated** with a new UUID because we set `default_factory=uuid.uuid4`. + +As `uuid.uuid4` will be called when creating the model instance, even before sending it to the database, we can **access and use the ID right away**. + +And that **same ID (a UUID)** will be saved in the database. + +//// tab | Python 3.10+ + +```Python hl_lines="5 7 9 14" +# Code above omitted 👆 + +{!./docs_src/advanced/uuid/tutorial001_py310.py[ln:23-34]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + +```Python hl_lines="5 7 9 14" +# Code above omitted 👆 + +{!./docs_src/advanced/uuid/tutorial001.py[ln:24-35]!} + +# Code below omitted 👇 +``` + +//// + +/// details | 👀 Full file preview + +//// tab | Python 3.10+ + +```Python +{!./docs_src/advanced/uuid/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + +```Python +{!./docs_src/advanced/uuid/tutorial001.py!} +``` + +//// + +/// + +### Select a Hero + +We can do the same operations we could do with other fields. + +For example we can **select a hero by ID**: + +//// tab | Python 3.10+ + +```Python hl_lines="15" +# Code above omitted 👆 + +{!./docs_src/advanced/uuid/tutorial001_py310.py[ln:37-54]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + +```Python hl_lines="15" +# Code above omitted 👆 + +{!./docs_src/advanced/uuid/tutorial001.py[ln:38-55]!} + +# Code below omitted 👇 +``` + +//// + +/// details | 👀 Full file preview + +//// tab | Python 3.10+ + +```Python +{!./docs_src/advanced/uuid/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + +```Python +{!./docs_src/advanced/uuid/tutorial001.py!} +``` + +//// + +/// + +/// tip + +Even if a database like SQLite stores the UUID as a string, we can select and run comparisons using a Python UUID object and it will work. + +SQLModel (actually SQLAlchemy) will take care of making it work. ✨ + +/// + +#### Select with `session.get()` + +We could also select by ID with `session.get()`: + +//// tab | Python 3.10+ + +```Python hl_lines="15" +# Code above omitted 👆 + +{!./docs_src/advanced/uuid/tutorial002_py310.py[ln:37-54]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + +```Python hl_lines="15" +# Code above omitted 👆 + +{!./docs_src/advanced/uuid/tutorial002.py[ln:38-55]!} + +# Code below omitted 👇 +``` + +//// + +/// details | 👀 Full file preview + +//// tab | Python 3.10+ + +```Python +{!./docs_src/advanced/uuid/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.7+ + +```Python +{!./docs_src/advanced/uuid/tutorial002.py!} +``` + +//// + +/// + +The same way as with other fields, we could update, delete, etc. 🚀 + +### Run the program + +If you run the program, you will see the **UUID** generated in the Python code, and then the record **saved in the database with the same UUID**. + +
+ +```console +$ python app.py + +// Some boilerplate and previous output omitted 😉 + +// In SQLite, the UUID will be stored as a string +// other DBs like Postgres have a specific UUID type +CREATE TABLE hero ( + id CHAR(32) NOT NULL, + name VARCHAR NOT NULL, + secret_name VARCHAR NOT NULL, + age INTEGER, + PRIMARY KEY (id) +) + +// Before saving in the DB we already have the UUID +The hero before saving in the DB +name='Deadpond' secret_name='Dive Wilson' id=UUID('0e44c1a6-88d3-4a35-8b8a-307faa2def28') age=None +The hero ID was already set +0e44c1a6-88d3-4a35-8b8a-307faa2def28 + +// The SQL statement to insert the record uses our UUID +INSERT INTO hero (id, name, secret_name, age) VALUES (?, ?, ?, ?) +('0e44c1a688d34a358b8a307faa2def28', 'Deadpond', 'Dive Wilson', None) + +// And indeed, the record was saved with the UUID we created 😎 +After saving in the DB +age=None id=UUID('0e44c1a6-88d3-4a35-8b8a-307faa2def28') name='Deadpond' secret_name='Dive Wilson' + +// Now we create a new hero (to select it in a bit) +Created hero: +age=None id=UUID('9d90d186-85db-4eaa-891a-def7b4ae2dab') name='Spider-Boy' secret_name='Pedro Parqueador' +Created hero ID: +9d90d186-85db-4eaa-891a-def7b4ae2dab + +// And now we select it +Selected hero: +age=None id=UUID('9d90d186-85db-4eaa-891a-def7b4ae2dab') name='Spider-Boy' secret_name='Pedro Parqueador' +Selected hero ID: +9d90d186-85db-4eaa-891a-def7b4ae2dab +``` + +
+ +## Learn More + +You can learn more about **UUIDs** in: + +* The official Python docs for UUID. +* The Wikipedia for UUID. diff --git a/docs_src/advanced/uuid/__init__.py b/docs_src/advanced/uuid/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/advanced/uuid/tutorial001.py b/docs_src/advanced/uuid/tutorial001.py new file mode 100644 index 0000000000..cfd3146b41 --- /dev/null +++ b/docs_src/advanced/uuid/tutorial001.py @@ -0,0 +1,65 @@ +import uuid +from typing import Union + +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Union[int, None] = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_hero(): + with Session(engine) as session: + hero = Hero(name="Deadpond", secret_name="Dive Wilson") + print("The hero before saving in the DB") + print(hero) + print("The hero ID was already set") + print(hero.id) + session.add(hero) + session.commit() + session.refresh(hero) + print("After saving in the DB") + print(hero) + + +def select_hero(): + with Session(engine) as session: + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_2) + session.commit() + session.refresh(hero_2) + hero_id = hero_2.id + print("Created hero:") + print(hero_2) + print("Created hero ID:") + print(hero_id) + + statement = select(Hero).where(Hero.id == hero_id) + selected_hero = session.exec(statement).one() + print("Selected hero:") + print(selected_hero) + print("Selected hero ID:") + print(selected_hero.id) + + +def main() -> None: + create_db_and_tables() + create_hero() + select_hero() + + +if __name__ == "__main__": + main() diff --git a/docs_src/advanced/uuid/tutorial001_py310.py b/docs_src/advanced/uuid/tutorial001_py310.py new file mode 100644 index 0000000000..610ec6b0d4 --- /dev/null +++ b/docs_src/advanced/uuid/tutorial001_py310.py @@ -0,0 +1,64 @@ +import uuid + +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +class Hero(SQLModel, table=True): + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_hero(): + with Session(engine) as session: + hero = Hero(name="Deadpond", secret_name="Dive Wilson") + print("The hero before saving in the DB") + print(hero) + print("The hero ID was already set") + print(hero.id) + session.add(hero) + session.commit() + session.refresh(hero) + print("After saving in the DB") + print(hero) + + +def select_hero(): + with Session(engine) as session: + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_2) + session.commit() + session.refresh(hero_2) + hero_id = hero_2.id + print("Created hero:") + print(hero_2) + print("Created hero ID:") + print(hero_id) + + statement = select(Hero).where(Hero.id == hero_id) + selected_hero = session.exec(statement).one() + print("Selected hero:") + print(selected_hero) + print("Selected hero ID:") + print(selected_hero.id) + + +def main() -> None: + create_db_and_tables() + create_hero() + select_hero() + + +if __name__ == "__main__": + main() diff --git a/docs_src/advanced/uuid/tutorial002.py b/docs_src/advanced/uuid/tutorial002.py new file mode 100644 index 0000000000..831725581b --- /dev/null +++ b/docs_src/advanced/uuid/tutorial002.py @@ -0,0 +1,64 @@ +import uuid +from typing import Union + +from sqlmodel import Field, Session, SQLModel, create_engine + + +class Hero(SQLModel, table=True): + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Union[int, None] = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_hero(): + with Session(engine) as session: + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + print("The hero before saving in the DB") + print(hero_1) + print("The hero ID was already set") + print(hero_1.id) + session.add(hero_1) + session.commit() + session.refresh(hero_1) + print("After saving in the DB") + print(hero_1) + + +def select_hero(): + with Session(engine) as session: + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_2) + session.commit() + session.refresh(hero_2) + hero_id = hero_2.id + print("Created hero:") + print(hero_2) + print("Created hero ID:") + print(hero_id) + + selected_hero = session.get(Hero, hero_id) + print("Selected hero:") + print(selected_hero) + print("Selected hero ID:") + print(selected_hero.id) + + +def main() -> None: + create_db_and_tables() + create_hero() + select_hero() + + +if __name__ == "__main__": + main() diff --git a/docs_src/advanced/uuid/tutorial002_py310.py b/docs_src/advanced/uuid/tutorial002_py310.py new file mode 100644 index 0000000000..3ec8c80fa0 --- /dev/null +++ b/docs_src/advanced/uuid/tutorial002_py310.py @@ -0,0 +1,63 @@ +import uuid + +from sqlmodel import Field, Session, SQLModel, create_engine + + +class Hero(SQLModel, table=True): + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_hero(): + with Session(engine) as session: + hero = Hero(name="Deadpond", secret_name="Dive Wilson") + print("The hero before saving in the DB") + print(hero) + print("The hero ID was already set") + print(hero.id) + session.add(hero) + session.commit() + session.refresh(hero) + print("After saving in the DB") + print(hero) + + +def select_hero(): + with Session(engine) as session: + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_2) + session.commit() + session.refresh(hero_2) + hero_id = hero_2.id + print("Created hero:") + print(hero_2) + print("Created hero ID:") + print(hero_id) + + selected_hero = session.get(Hero, hero_id) + print("Selected hero:") + print(selected_hero) + print("Selected hero ID:") + print(selected_hero.id) + + +def main() -> None: + create_db_and_tables() + create_hero() + select_hero() + + +if __name__ == "__main__": + main() diff --git a/mkdocs.yml b/mkdocs.yml index fa85062a8b..09c2b7f156 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -99,6 +99,7 @@ nav: - Advanced User Guide: - advanced/index.md - advanced/decimal.md + - advanced/uuid.md - alternatives.md - help.md - contributing.md diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index 61ae35f7eb..5983974d77 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -140,5 +140,4 @@ from .sql.expression import tuple_ as tuple_ from .sql.expression import type_coerce as type_coerce from .sql.expression import within_group as within_group -from .sql.sqltypes import GUID as GUID from .sql.sqltypes import AutoString as AutoString diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 505683f756..5755bbb4fd 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -51,7 +51,7 @@ from sqlalchemy.orm.decl_api import DeclarativeMeta from sqlalchemy.orm.instrumentation import is_instrumented from sqlalchemy.sql.schema import MetaData -from sqlalchemy.sql.sqltypes import LargeBinary, Time +from sqlalchemy.sql.sqltypes import LargeBinary, Time, Uuid from typing_extensions import Literal, deprecated, get_origin from ._compat import ( # type: ignore[attr-defined] @@ -80,7 +80,7 @@ sqlmodel_init, sqlmodel_validate, ) -from .sql.sqltypes import GUID, AutoString +from .sql.sqltypes import AutoString if TYPE_CHECKING: from pydantic._internal._model_construction import ModelMetaclass as ModelMetaclass @@ -608,7 +608,7 @@ def get_sqlalchemy_type(field: Any) -> Any: scale=getattr(metadata, "decimal_places", None), ) if issubclass(type_, uuid.UUID): - return GUID + return Uuid raise ValueError(f"{type_} has no matching SQLAlchemy type") diff --git a/sqlmodel/sql/sqltypes.py b/sqlmodel/sql/sqltypes.py index 5a4bb04ef1..512daacbab 100644 --- a/sqlmodel/sql/sqltypes.py +++ b/sqlmodel/sql/sqltypes.py @@ -1,10 +1,7 @@ -import uuid -from typing import Any, Optional, cast +from typing import Any, cast -from sqlalchemy import CHAR, types -from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy import types from sqlalchemy.engine.interfaces import Dialect -from sqlalchemy.sql.type_api import TypeEngine class AutoString(types.TypeDecorator): # type: ignore @@ -17,43 +14,3 @@ def load_dialect_impl(self, dialect: Dialect) -> "types.TypeEngine[Any]": if impl.length is None and dialect.name == "mysql": return dialect.type_descriptor(types.String(self.mysql_default_length)) return super().load_dialect_impl(dialect) - - -# Reference form SQLAlchemy docs: https://docs.sqlalchemy.org/en/14/core/custom_types.html#backend-agnostic-guid-type -# with small modifications -class GUID(types.TypeDecorator): # type: ignore - """Platform-independent GUID type. - - Uses PostgreSQL's UUID type, otherwise uses - CHAR(32), storing as stringified hex values. - - """ - - impl = CHAR - cache_ok = True - - def load_dialect_impl(self, dialect: Dialect) -> TypeEngine[Any]: - if dialect.name == "postgresql": - return dialect.type_descriptor(UUID()) - else: - return dialect.type_descriptor(CHAR(32)) - - def process_bind_param(self, value: Any, dialect: Dialect) -> Optional[str]: - if value is None: - return value - elif dialect.name == "postgresql": - return str(value) - else: - if not isinstance(value, uuid.UUID): - return uuid.UUID(value).hex - else: - # hexstring - return value.hex - - def process_result_value(self, value: Any, dialect: Dialect) -> Optional[uuid.UUID]: - if value is None: - return value - else: - if not isinstance(value, uuid.UUID): - value = uuid.UUID(value) - return cast(uuid.UUID, value) diff --git a/tests/test_advanced/test_uuid/__init__.py b/tests/test_advanced/test_uuid/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_advanced/test_uuid/test_tutorial001.py b/tests/test_advanced/test_uuid/test_tutorial001.py new file mode 100644 index 0000000000..405195f8e9 --- /dev/null +++ b/tests/test_advanced/test_uuid/test_tutorial001.py @@ -0,0 +1,71 @@ +from unittest.mock import patch + +from dirty_equals import IsUUID +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function + + +def test_tutorial(clear_sqlmodel) -> None: + from docs_src.advanced.uuid import tutorial001 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + first_uuid = calls[1][0]["id"] + assert first_uuid == IsUUID(4) + + second_uuid = calls[7][0]["id"] + assert second_uuid == IsUUID(4) + + assert first_uuid != second_uuid + + assert calls == [ + ["The hero before saving in the DB"], + [ + { + "name": "Deadpond", + "secret_name": "Dive Wilson", + "id": first_uuid, + "age": None, + } + ], + ["The hero ID was already set"], + [first_uuid], + ["After saving in the DB"], + [ + { + "name": "Deadpond", + "secret_name": "Dive Wilson", + "age": None, + "id": first_uuid, + } + ], + ["Created hero:"], + [ + { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "age": None, + "id": second_uuid, + } + ], + ["Created hero ID:"], + [second_uuid], + ["Selected hero:"], + [ + { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "age": None, + "id": second_uuid, + } + ], + ["Selected hero ID:"], + [second_uuid], + ] diff --git a/tests/test_advanced/test_uuid/test_tutorial001_py310.py b/tests/test_advanced/test_uuid/test_tutorial001_py310.py new file mode 100644 index 0000000000..ee8cb085df --- /dev/null +++ b/tests/test_advanced/test_uuid/test_tutorial001_py310.py @@ -0,0 +1,72 @@ +from unittest.mock import patch + +from dirty_equals import IsUUID +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel) -> None: + from docs_src.advanced.uuid import tutorial001_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + first_uuid = calls[1][0]["id"] + assert first_uuid == IsUUID(4) + + second_uuid = calls[7][0]["id"] + assert second_uuid == IsUUID(4) + + assert first_uuid != second_uuid + + assert calls == [ + ["The hero before saving in the DB"], + [ + { + "name": "Deadpond", + "secret_name": "Dive Wilson", + "id": first_uuid, + "age": None, + } + ], + ["The hero ID was already set"], + [first_uuid], + ["After saving in the DB"], + [ + { + "name": "Deadpond", + "secret_name": "Dive Wilson", + "age": None, + "id": first_uuid, + } + ], + ["Created hero:"], + [ + { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "age": None, + "id": second_uuid, + } + ], + ["Created hero ID:"], + [second_uuid], + ["Selected hero:"], + [ + { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "age": None, + "id": second_uuid, + } + ], + ["Selected hero ID:"], + [second_uuid], + ] diff --git a/tests/test_advanced/test_uuid/test_tutorial002.py b/tests/test_advanced/test_uuid/test_tutorial002.py new file mode 100644 index 0000000000..cefd95ba49 --- /dev/null +++ b/tests/test_advanced/test_uuid/test_tutorial002.py @@ -0,0 +1,71 @@ +from unittest.mock import patch + +from dirty_equals import IsUUID +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function + + +def test_tutorial(clear_sqlmodel) -> None: + from docs_src.advanced.uuid import tutorial002 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + first_uuid = calls[1][0]["id"] + assert first_uuid == IsUUID(4) + + second_uuid = calls[7][0]["id"] + assert second_uuid == IsUUID(4) + + assert first_uuid != second_uuid + + assert calls == [ + ["The hero before saving in the DB"], + [ + { + "name": "Deadpond", + "secret_name": "Dive Wilson", + "id": first_uuid, + "age": None, + } + ], + ["The hero ID was already set"], + [first_uuid], + ["After saving in the DB"], + [ + { + "name": "Deadpond", + "secret_name": "Dive Wilson", + "age": None, + "id": first_uuid, + } + ], + ["Created hero:"], + [ + { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "age": None, + "id": second_uuid, + } + ], + ["Created hero ID:"], + [second_uuid], + ["Selected hero:"], + [ + { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "age": None, + "id": second_uuid, + } + ], + ["Selected hero ID:"], + [second_uuid], + ] diff --git a/tests/test_advanced/test_uuid/test_tutorial002_py310.py b/tests/test_advanced/test_uuid/test_tutorial002_py310.py new file mode 100644 index 0000000000..96f85c5333 --- /dev/null +++ b/tests/test_advanced/test_uuid/test_tutorial002_py310.py @@ -0,0 +1,72 @@ +from unittest.mock import patch + +from dirty_equals import IsUUID +from sqlmodel import create_engine + +from ...conftest import get_testing_print_function, needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel) -> None: + from docs_src.advanced.uuid import tutorial002_py310 as mod + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + first_uuid = calls[1][0]["id"] + assert first_uuid == IsUUID(4) + + second_uuid = calls[7][0]["id"] + assert second_uuid == IsUUID(4) + + assert first_uuid != second_uuid + + assert calls == [ + ["The hero before saving in the DB"], + [ + { + "name": "Deadpond", + "secret_name": "Dive Wilson", + "id": first_uuid, + "age": None, + } + ], + ["The hero ID was already set"], + [first_uuid], + ["After saving in the DB"], + [ + { + "name": "Deadpond", + "secret_name": "Dive Wilson", + "age": None, + "id": first_uuid, + } + ], + ["Created hero:"], + [ + { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "age": None, + "id": second_uuid, + } + ], + ["Created hero ID:"], + [second_uuid], + ["Selected hero:"], + [ + { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "age": None, + "id": second_uuid, + } + ], + ["Selected hero ID:"], + [second_uuid], + ] From fca0621098e634668457c04a7905e911c8ac1994 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 17 Jul 2024 01:52:20 +0000 Subject: [PATCH 400/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 8df3f80877..ec4f673ea4 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,10 @@ * ✏️ Fix typo in `docs/tutorial/relationship-attributes/index.md`. PR [#880](https://github.com/tiangolo/sqlmodel/pull/880) by [@UncleGoogle](https://github.com/UncleGoogle). +### Features + +* ✨ Add official UUID support, docs and tests, internally using new SQLAlchemy 2.0 types. PR [#992](https://github.com/tiangolo/sqlmodel/pull/992) by [@estebanx64](https://github.com/estebanx64). + ### Docs * ✏️ Fix internal link in `docs/tutorial/create-db-and-table.md`. PR [#911](https://github.com/tiangolo/sqlmodel/pull/911) by [@tfpgh](https://github.com/tfpgh). From 690f9cf5e1872b4857ebba11debed33c022487ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 16 Jul 2024 21:15:42 -0500 Subject: [PATCH 401/906] =?UTF-8?q?=F0=9F=94=A8=20Update=20docs=20Termynal?= =?UTF-8?q?=20scripts=20to=20not=20include=20line=20nums=20for=20local=20d?= =?UTF-8?q?ev=20(#1018)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/css/custom.css | 4 ++++ docs/js/custom.js | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/css/custom.css b/docs/css/custom.css index c479ad08f9..65265a5389 100644 --- a/docs/css/custom.css +++ b/docs/css/custom.css @@ -8,6 +8,10 @@ white-space: pre-wrap; } +.termy .linenos { + display: none; +} + a.external-link::after { /* \00A0 is a non-breaking space to make the mark be on the same line as the link diff --git a/docs/js/custom.js b/docs/js/custom.js index 58f321a05e..0dda9fdd54 100644 --- a/docs/js/custom.js +++ b/docs/js/custom.js @@ -13,7 +13,7 @@ function setupTermynal() { function createTermynals() { document - .querySelectorAll(`.${termynalActivateClass} .highlight`) + .querySelectorAll(`.${termynalActivateClass} .highlight code`) .forEach(node => { const text = node.textContent; const lines = text.split("\n"); From 09adc76e3db8c84a54fab8c27ab3e6816d5e0ccc Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 17 Jul 2024 02:16:15 +0000 Subject: [PATCH 402/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index ec4f673ea4..e24c673aac 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -16,6 +16,10 @@ * ✏️ Fix typo in `sqlmodel/_compat.py`. PR [#950](https://github.com/tiangolo/sqlmodel/pull/950) by [@Highfire1](https://github.com/Highfire1). * ✏️ Update pip installation command in tutorial. PR [#975](https://github.com/tiangolo/sqlmodel/pull/975) by [@alejsdev](https://github.com/alejsdev). +### Internal + +* 🔨 Update docs Termynal scripts to not include line nums for local dev. PR [#1018](https://github.com/tiangolo/sqlmodel/pull/1018) by [@tiangolo](https://github.com/tiangolo). + ## 0.0.19 ### Fixes From 1920f070529ef201dee5b06913e25f18d823686e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 16 Jul 2024 21:21:01 -0500 Subject: [PATCH 403/906] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#979)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.5.0 → v4.6.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.5.0...v4.6.0) - [github.com/astral-sh/ruff-pre-commit: v0.4.7 → v0.5.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.7...v0.5.2) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6b2ad2b895..d8828f4421 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ default_language_version: python: python3.10 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: check-added-large-files - id: check-toml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.7 + rev: v0.5.2 hooks: - id: ruff args: From 7ba80e47e73f5d574c80e430a52c9fd196ffb2ca Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 17 Jul 2024 02:21:19 +0000 Subject: [PATCH 404/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index e24c673aac..f60ca05a1c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -18,6 +18,7 @@ ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#979](https://github.com/tiangolo/sqlmodel/pull/979) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * 🔨 Update docs Termynal scripts to not include line nums for local dev. PR [#1018](https://github.com/tiangolo/sqlmodel/pull/1018) by [@tiangolo](https://github.com/tiangolo). ## 0.0.19 From 438480f1286592629c87b42eeca480a0e871082d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 16 Jul 2024 21:52:43 -0500 Subject: [PATCH 405/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index f60ca05a1c..3e2cd7f058 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,11 +2,10 @@ ## Latest Changes -* ✏️ Fix typo in `docs/tutorial/relationship-attributes/index.md`. PR [#880](https://github.com/tiangolo/sqlmodel/pull/880) by [@UncleGoogle](https://github.com/UncleGoogle). - ### Features -* ✨ Add official UUID support, docs and tests, internally using new SQLAlchemy 2.0 types. PR [#992](https://github.com/tiangolo/sqlmodel/pull/992) by [@estebanx64](https://github.com/estebanx64). +* ✨ Add official UUID support, docs and tests, internally using new SQLAlchemy 2.0 types. Initial PR [#992](https://github.com/tiangolo/sqlmodel/pull/992) by [@estebanx64](https://github.com/estebanx64). + * New docs in the [Advanced User Guide: UUID (Universally Unique Identifiers)](https://sqlmodel.tiangolo.com/advanced/uuid/). ### Docs @@ -15,6 +14,7 @@ * ✏️ Fix typo in `docs/tutorial`. PR [#943](https://github.com/tiangolo/sqlmodel/pull/943) by [@luco17](https://github.com/luco17). * ✏️ Fix typo in `sqlmodel/_compat.py`. PR [#950](https://github.com/tiangolo/sqlmodel/pull/950) by [@Highfire1](https://github.com/Highfire1). * ✏️ Update pip installation command in tutorial. PR [#975](https://github.com/tiangolo/sqlmodel/pull/975) by [@alejsdev](https://github.com/alejsdev). +* ✏️ Fix typo in `docs/tutorial/relationship-attributes/index.md`. PR [#880](https://github.com/tiangolo/sqlmodel/pull/880) by [@UncleGoogle](https://github.com/UncleGoogle). ### Internal From b8d7f4ff67939e36b1847df2308e1ed3772fc813 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 16 Jul 2024 21:53:24 -0500 Subject: [PATCH 406/906] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.0.?= =?UTF-8?q?20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 2 ++ sqlmodel/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 3e2cd7f058..086dc79bac 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest Changes +## 0.0.20 + ### Features * ✨ Add official UUID support, docs and tests, internally using new SQLAlchemy 2.0 types. Initial PR [#992](https://github.com/tiangolo/sqlmodel/pull/992) by [@estebanx64](https://github.com/estebanx64). diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index 5983974d77..438d4ba07e 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.0.19" +__version__ = "0.0.20" # Re-export from SQLAlchemy from sqlalchemy.engine import create_engine as create_engine From da1253c21f459f52af96936525ee155a9490fdb0 Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Tue, 16 Jul 2024 22:03:52 -0500 Subject: [PATCH 407/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20docs=20(#1003)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tutorial/automatic-id-none-refresh.md | 2 +- docs/tutorial/create-db-and-table.md | 2 +- .../relationship-attributes/create-and-update-relationships.md | 2 +- docs/tutorial/where.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/tutorial/automatic-id-none-refresh.md b/docs/tutorial/automatic-id-none-refresh.md index 1f98a76cfe..1963492359 100644 --- a/docs/tutorial/automatic-id-none-refresh.md +++ b/docs/tutorial/automatic-id-none-refresh.md @@ -4,7 +4,7 @@ In the previous chapter, we saw how to add rows to the database using **SQLModel Now let's talk a bit about why the `id` field **can't be `NULL`** on the database because it's a **primary key**, and we declare it using `Field(primary_key=True)`. -But the same `id` field actually **can be `None`** in the Python code, so we declare the type with `Optional[int]`, and set the default value to `Field(default=None)`: +But the same `id` field actually **can be `None`** in the Python code, so we declare the type with `int | None (or Optional[int])`, and set the default value to `Field(default=None)`: //// tab | Python 3.10+ diff --git a/docs/tutorial/create-db-and-table.md b/docs/tutorial/create-db-and-table.md index 761c979e02..de820ab760 100644 --- a/docs/tutorial/create-db-and-table.md +++ b/docs/tutorial/create-db-and-table.md @@ -145,7 +145,7 @@ Let's now see with more detail these field/column declarations. ### Optional Fields, Nullable Columns -Let's start with `age`, notice that it has a type of `Optional[int]`. +Let's start with `age`, notice that it has a type of `int | None (or Optional[int])`. And we import that `Optional` from the `typing` standard module. diff --git a/docs/tutorial/relationship-attributes/create-and-update-relationships.md b/docs/tutorial/relationship-attributes/create-and-update-relationships.md index 8b34f0cb53..ce2f4d87d4 100644 --- a/docs/tutorial/relationship-attributes/create-and-update-relationships.md +++ b/docs/tutorial/relationship-attributes/create-and-update-relationships.md @@ -134,7 +134,7 @@ Now let's do all that, but this time using the new, shiny `Relationship` attribu Now we can create the `Team` instances and pass them directly to the new `team` argument when creating the `Hero` instances, as `team=team_preventers` instead of `team_id=team_preventers.id`. -And thanks to SQLAlchemy and how it works underneath, these teams don't even have to have an ID yet, but because we are assigning the whole object to each hero, those teams **will be automatically created** in the database, the automatic ID will be generated, and will be set in the `team_id` column for each of the corresponding hero rows. +And thanks to SQLAlchemy and how it works underneath, these teams don't even need to have an ID yet, but because we are assigning the whole object to each hero, those teams **will be automatically created** in the database, the automatic ID will be generated, and will be set in the `team_id` column for each of the corresponding hero rows. In fact, now we don't even have to put the teams explicitly in the session with `session.add(team)`, because these `Team` instances are **already associated** with heroes that **we do** `add` to the session. diff --git a/docs/tutorial/where.md b/docs/tutorial/where.md index 46306561b4..5bc3139b57 100644 --- a/docs/tutorial/where.md +++ b/docs/tutorial/where.md @@ -1250,7 +1250,7 @@ It would be an error telling you that > `Hero.age` is potentially `None`, and you cannot compare `None` with `>` -This is because as we are using pure and plain Python annotations for the fields, `age` is indeed annotated as `Optional[int]`, which means `int` or `None`. +This is because as we are using pure and plain Python annotations for the fields, `age` is indeed annotated as `int | None (or Optional[int])`. By using this simple and standard Python type annotations we get the benefit of the extra simplicity and the inline error checks when creating or using instances. ✨ From 3c75056c6e7c66a7f7b3655e1304aa165c137d5a Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 17 Jul 2024 03:04:11 +0000 Subject: [PATCH 408/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 086dc79bac..be356c710c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Docs + +* 📝 Update docs . PR [#1003](https://github.com/tiangolo/sqlmodel/pull/1003) by [@alejsdev](https://github.com/alejsdev). + ## 0.0.20 ### Features From a23e36bec97aa74c19f829bae69b941332be4b1a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Jul 2024 22:06:53 -0500 Subject: [PATCH 409/906] =?UTF-8?q?=E2=AC=86=20Bump=20dorny/paths-filter?= =?UTF-8?q?=20from=202=20to=203=20(#972)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [dorny/paths-filter](https://github.com/dorny/paths-filter) from 2 to 3. - [Release notes](https://github.com/dorny/paths-filter/releases) - [Changelog](https://github.com/dorny/paths-filter/blob/master/CHANGELOG.md) - [Commits](https://github.com/dorny/paths-filter/compare/v2...v3) --- updated-dependencies: - dependency-name: dorny/paths-filter dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 16da4a2da3..af1639fcb4 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -19,7 +19,7 @@ jobs: steps: - uses: actions/checkout@v4 # For pull requests it's not necessary to checkout the code but for the main branch it is - - uses: dorny/paths-filter@v2 + - uses: dorny/paths-filter@v3 id: filter with: filters: | From 8792c442d7ff6ad82f3b86ef6f809c01f1755060 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 17 Jul 2024 03:07:13 +0000 Subject: [PATCH 410/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index be356c710c..9ce22af9a8 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,6 +6,10 @@ * 📝 Update docs . PR [#1003](https://github.com/tiangolo/sqlmodel/pull/1003) by [@alejsdev](https://github.com/alejsdev). +### Internal + +* ⬆ Bump dorny/paths-filter from 2 to 3. PR [#972](https://github.com/tiangolo/sqlmodel/pull/972) by [@dependabot[bot]](https://github.com/apps/dependabot). + ## 0.0.20 ### Features From eb92b8315dca049b857edc597adfe9c2c65148b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Jul 2024 22:07:28 -0500 Subject: [PATCH 411/906] =?UTF-8?q?=E2=AC=86=20Bump=20mkdocstrings[python]?= =?UTF-8?q?=20from=200.23.0=20to=200.25.1=20(#927)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mkdocstrings[python]](https://github.com/mkdocstrings/mkdocstrings) from 0.23.0 to 0.25.1. - [Release notes](https://github.com/mkdocstrings/mkdocstrings/releases) - [Changelog](https://github.com/mkdocstrings/mkdocstrings/blob/main/CHANGELOG.md) - [Commits](https://github.com/mkdocstrings/mkdocstrings/compare/0.23.0...0.25.1) --- updated-dependencies: - dependency-name: mkdocstrings[python] dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index d3a1838af8..9b76e626b8 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -11,7 +11,7 @@ jieba==0.42.1 pillow==10.1.0 # For image processing by Material for MkDocs cairosvg==2.7.0 -mkdocstrings[python]==0.23.0 +mkdocstrings[python]==0.25.1 # Enable griffe-typingdoc once dropping Python 3.7 and upgrading typing-extensions # griffe-typingdoc==0.2.5 # For griffe, it formats with black From 58d9d2c3a603dde444ff07414531a8c7c16d6cd9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Jul 2024 22:08:25 -0500 Subject: [PATCH 412/906] =?UTF-8?q?=E2=AC=86=20Bump=20pypa/gh-action-pypi-?= =?UTF-8?q?publish=20from=201.8.11=20to=201.9.0=20(#987)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.8.11 to 1.9.0. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.8.11...v1.9.0) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1397e17ae6..10be182e6b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -34,4 +34,4 @@ jobs: TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }} run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.8.11 + uses: pypa/gh-action-pypi-publish@v1.9.0 From 814d3a4b14f10c0a219449946e3576ddf88d9fc8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 17 Jul 2024 03:09:30 +0000 Subject: [PATCH 413/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 9ce22af9a8..5bb3385eda 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* ⬆ Bump mkdocstrings[python] from 0.23.0 to 0.25.1. PR [#927](https://github.com/tiangolo/sqlmodel/pull/927) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump dorny/paths-filter from 2 to 3. PR [#972](https://github.com/tiangolo/sqlmodel/pull/972) by [@dependabot[bot]](https://github.com/apps/dependabot). ## 0.0.20 From 50b16972fe56f9fd122e410e4d2af3252f04eb5f Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 17 Jul 2024 03:10:35 +0000 Subject: [PATCH 414/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 5bb3385eda..e83d55a146 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* ⬆ Bump pypa/gh-action-pypi-publish from 1.8.11 to 1.9.0. PR [#987](https://github.com/tiangolo/sqlmodel/pull/987) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocstrings[python] from 0.23.0 to 0.25.1. PR [#927](https://github.com/tiangolo/sqlmodel/pull/927) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump dorny/paths-filter from 2 to 3. PR [#972](https://github.com/tiangolo/sqlmodel/pull/972) by [@dependabot[bot]](https://github.com/apps/dependabot). From eaa9023049fafc3fbb0de35ccf01bcff3ff31bab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Jul 2024 03:15:45 +0000 Subject: [PATCH 415/906] =?UTF-8?q?=E2=AC=86=20Bump=20jinja2=20from=203.1.?= =?UTF-8?q?3=20to=203.1.4=20(#974)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.3 to 3.1.4. - [Release notes](https://github.com/pallets/jinja/releases) - [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/jinja/compare/3.1.3...3.1.4) --- updated-dependencies: - dependency-name: jinja2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 8801bb9b91..089e1a8326 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -9,7 +9,7 @@ fastapi >=0.103.2 httpx ==0.24.1 # TODO: upgrade when deprecating Python 3.7 dirty-equals ==0.6.0 -jinja2 ==3.1.3 +jinja2 ==3.1.4 # Pin typing-extensions until Python 3.8 is deprecated or the issue with dirty-equals # is fixed, maybe fixed after dropping Python 3.7 and upgrading dirty-equals typing-extensions ==4.6.1 From eb5fb78114029d56e700a655bf645c739ba88de0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 17 Jul 2024 03:16:06 +0000 Subject: [PATCH 416/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index e83d55a146..a3f34b4ebd 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* ⬆ Bump jinja2 from 3.1.3 to 3.1.4. PR [#974](https://github.com/tiangolo/sqlmodel/pull/974) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pypa/gh-action-pypi-publish from 1.8.11 to 1.9.0. PR [#987](https://github.com/tiangolo/sqlmodel/pull/987) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocstrings[python] from 0.23.0 to 0.25.1. PR [#927](https://github.com/tiangolo/sqlmodel/pull/927) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump dorny/paths-filter from 2 to 3. PR [#972](https://github.com/tiangolo/sqlmodel/pull/972) by [@dependabot[bot]](https://github.com/apps/dependabot). From 54d8a685cc85fa4511f632a5edd96b68215a029c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Jul 2024 03:18:16 +0000 Subject: [PATCH 417/906] =?UTF-8?q?=E2=AC=86=20Bump=20cairosvg=20from=202.?= =?UTF-8?q?7.0=20to=202.7.1=20(#919)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [cairosvg](https://github.com/Kozea/CairoSVG) from 2.7.0 to 2.7.1. - [Release notes](https://github.com/Kozea/CairoSVG/releases) - [Changelog](https://github.com/Kozea/CairoSVG/blob/main/NEWS.rst) - [Commits](https://github.com/Kozea/CairoSVG/compare/2.7.0...2.7.1) --- updated-dependencies: - dependency-name: cairosvg dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 9b76e626b8..f30c24573d 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -10,7 +10,7 @@ jieba==0.42.1 # For image processing by Material for MkDocs pillow==10.1.0 # For image processing by Material for MkDocs -cairosvg==2.7.0 +cairosvg==2.7.1 mkdocstrings[python]==0.25.1 # Enable griffe-typingdoc once dropping Python 3.7 and upgrading typing-extensions # griffe-typingdoc==0.2.5 From 179b7781c2caaa977996ab3a4f9fad31ddb635d8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 17 Jul 2024 03:18:45 +0000 Subject: [PATCH 418/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index a3f34b4ebd..ad4ada30d4 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* ⬆ Bump cairosvg from 2.7.0 to 2.7.1. PR [#919](https://github.com/tiangolo/sqlmodel/pull/919) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump jinja2 from 3.1.3 to 3.1.4. PR [#974](https://github.com/tiangolo/sqlmodel/pull/974) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pypa/gh-action-pypi-publish from 1.8.11 to 1.9.0. PR [#987](https://github.com/tiangolo/sqlmodel/pull/987) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocstrings[python] from 0.23.0 to 0.25.1. PR [#927](https://github.com/tiangolo/sqlmodel/pull/927) by [@dependabot[bot]](https://github.com/apps/dependabot). From b8718c3b4d3193d1ecba3e77090ffae10e5613b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Jul 2024 18:52:15 -0500 Subject: [PATCH 419/906] =?UTF-8?q?=E2=AC=86=20Bump=20actions/cache=20from?= =?UTF-8?q?=203=20to=204=20(#783)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-docs.yml | 4 ++-- .github/workflows/test.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index af1639fcb4..d4bb6cf67b 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -49,7 +49,7 @@ jobs: uses: actions/setup-python@v5 with: python-version: "3.11" - - uses: actions/cache@v3 + - uses: actions/cache@v4 id: cache with: path: ${{ env.pythonLocation }} @@ -63,7 +63,7 @@ jobs: pip install git+https://${{ secrets.SQLMODEL_MKDOCS_MATERIAL_INSIDERS }}@github.com/squidfunk/mkdocs-material-insiders.git pip install git+https://${{ secrets.SQLMODEL_MKDOCS_MATERIAL_INSIDERS }}@github.com/pawamoy-insiders/griffe-typing-deprecated.git pip install git+https://${{ secrets.SQLMODEL_MKDOCS_MATERIAL_INSIDERS }}@github.com/pawamoy-insiders/mkdocstrings-python.git - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: key: mkdocs-cards-${{ github.ref }} path: .cache diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a1c9b36e1c..8fbbc324b4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -47,7 +47,7 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true - - uses: actions/cache@v3 + - uses: actions/cache@v4 id: cache with: path: ${{ env.pythonLocation }} From 19c736766ee140ff8579a4f1cfffc1ed007d1af0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 17 Jul 2024 23:52:33 +0000 Subject: [PATCH 420/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index ad4ada30d4..7184e98db3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* ⬆ Bump actions/cache from 3 to 4. PR [#783](https://github.com/tiangolo/sqlmodel/pull/783) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump cairosvg from 2.7.0 to 2.7.1. PR [#919](https://github.com/tiangolo/sqlmodel/pull/919) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump jinja2 from 3.1.3 to 3.1.4. PR [#974](https://github.com/tiangolo/sqlmodel/pull/974) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pypa/gh-action-pypi-publish from 1.8.11 to 1.9.0. PR [#987](https://github.com/tiangolo/sqlmodel/pull/987) by [@dependabot[bot]](https://github.com/apps/dependabot). From 86ab09f7ec30f5711722d894130d6c93b7f403ac Mon Sep 17 00:00:00 2001 From: Esteban Maya Date: Sat, 20 Jul 2024 21:08:06 -0500 Subject: [PATCH 421/906] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20casca?= =?UTF-8?q?de=20delete=20relationships:=20`cascade=5Fdelete`,=20`ondelete`?= =?UTF-8?q?,=20and=20`passive=5Fdeletes`=20(#983)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- .../cascade-delete-relationships.md | 1304 +++++++++++++++++ .../cascade_delete_relationships/__init__.py | 0 .../tutorial001.py | 110 ++ .../tutorial001_py310.py | 106 ++ .../tutorial001_py39.py | 110 ++ .../tutorial002.py | 110 ++ .../tutorial002_py310.py | 108 ++ .../tutorial002_py39.py | 110 ++ .../tutorial003.py | 112 ++ .../tutorial003_py310.py | 110 ++ .../tutorial003_py39.py | 112 ++ .../tutorial004.py | 111 ++ .../tutorial004_py310.py | 109 ++ .../tutorial004_py39.py | 111 ++ .../tutorial005.py | 124 ++ .../tutorial005_py310.py | 122 ++ .../tutorial005_py39.py | 124 ++ mkdocs.yml | 1 + sqlmodel/main.py | 95 +- tests/test_field_sa_column.py | 11 + tests/test_ondelete_raises.py | 37 + .../__init__.py | 0 .../test_tutorial001.py | 72 + .../test_tutorial001_py310.py | 73 + .../test_tutorial001_py39.py | 73 + .../test_tutorial002.py | 90 ++ .../test_tutorial002_py310.py | 91 ++ .../test_tutorial002_py39.py | 91 ++ .../test_tutorial003.py | 90 ++ .../test_tutorial003_py310.py | 91 ++ .../test_tutorial003_py39.py | 91 ++ .../test_tutorial004.py | 106 ++ .../test_tutorial004_py310.py | 107 ++ .../test_tutorial004_py39.py | 107 ++ .../test_tutorial005.py | 94 ++ .../test_tutorial005_py310.py | 95 ++ .../test_tutorial005_py39.py | 95 ++ 37 files changed, 4501 insertions(+), 2 deletions(-) create mode 100644 docs/tutorial/relationship-attributes/cascade-delete-relationships.md create mode 100644 docs_src/tutorial/relationship_attributes/cascade_delete_relationships/__init__.py create mode 100644 docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001.py create mode 100644 docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py310.py create mode 100644 docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py39.py create mode 100644 docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002.py create mode 100644 docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py310.py create mode 100644 docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py39.py create mode 100644 docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003.py create mode 100644 docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py310.py create mode 100644 docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py39.py create mode 100644 docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004.py create mode 100644 docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py310.py create mode 100644 docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py39.py create mode 100644 docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005.py create mode 100644 docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005_py310.py create mode 100644 docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005_py39.py create mode 100644 tests/test_ondelete_raises.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/__init__.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001_py39.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002_py310.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002_py39.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003_py310.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003_py39.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004_py310.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004_py39.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005_py310.py create mode 100644 tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005_py39.py diff --git a/docs/tutorial/relationship-attributes/cascade-delete-relationships.md b/docs/tutorial/relationship-attributes/cascade-delete-relationships.md new file mode 100644 index 0000000000..769d94cf99 --- /dev/null +++ b/docs/tutorial/relationship-attributes/cascade-delete-relationships.md @@ -0,0 +1,1304 @@ +# Cascade Delete Relationships + +What happens if we **delete** a team that has a **relationship** with heroes? + +Should those heroes be **automatically deleted** too? That's called a "**cascade**", because the initial deletion causes a cascade of other deletions. + +Should their `team_id` instead be set to `NULL` in the database? + +Let's see how to configure that with **SQLModel**. + +/// info + +This feature, including `cascade_delete`, `ondelete`, and `passive_deletes`, is available since SQLModel version `0.0.21`. + +/// + +## Initial Heroes and Teams + +Let's say that we have these **teams** and **heroes**. + +### Team Table + +| id | name | headquarters | +| ---- | ---------- | --------------------- | +| 1 | Z-Force | Sister Margaret's Bar | +| 2 | Preventers | Sharp Tower | +| 3 | Wakaland | Wakaland Capital City | + +### Hero Table + +| id | name | secret_name | age | team_id | +| ---- | --------------- | ---------------- | ---- | ------- | +| 1 | Deadpond | Dive WIlson | | 1 | +| 2 | Rusty-Man | Tommy Sharp | 48 | 2 | +| 3 | Spider-Boy | Pedro Parqueador | | 2 | +| 4 | Black Lion | Trevor Challa | 35 | 3 | +| 5 | Princess Sure-E | Sure-E | | 3 | + +### Visual Teams and Heroes + +We could visualize them like this: + +```mermaid +flowchart TB + subgraph "Z-Force" + d("Deadpond") + end + subgraph "Preventers" + r("Rusty-Man") + s("Spider-Boy") + end + subgraph "Wakaland" + b("Black Lion") + p("Princess Sure-E") + end +``` + +## Delete a Team with Heroes + +When we **delete a team**, we have to do something with the associated heroes. + +By default, their foreign key pointing to the team will be set to `NULL` in the database. + +But let's say we want the associated heroes to be **automatically deleted**. + +For example, we could delete the team `Wakaland`: + +```mermaid +flowchart TB + subgraph zforce["Z-Force"] + d("Deadpond") + end + subgraph preventers["Preventers"] + r("Rusty-Man") + s("Spider-Boy") + end + subgraph wakaland["Wakaland"] + b("Black Lion") + p("Princess Sure-E") + end + style wakaland fill:#fee,stroke:#900 +``` + +And we would want the heroes `Black Lion` and `Princess Sure-E` to be **automatically deleted** too. + +So we would end up with these teams and heroes: + +```mermaid +flowchart TB + subgraph zforce["Team Z-Force"] + d("Deadpond") + end + subgraph preventers["Team Preventers"] + r("Rusty-Man") + s("Spider-Boy") + end +``` + +## Configure Automatic Deletion + +There are **two places** where this automatic deletion is configured: + +* in **Python code** +* in the **database** + +## Delete in Python with `cascade_delete` + +When creating a `Relationship()`, we can set `cascade_delete=True`. + +This configures SQLModel to **automatically delete** the related records (heroes) **when the initial one is deleted** (a team). + +//// tab | Python 3.10+ + +```Python hl_lines="9" +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py310.py[ln:1-9]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="11" +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py39.py[ln:1-11]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + +```Python hl_lines="11" +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001.py[ln:1-11]!} + +# Code below omitted 👇 +``` + +//// + +/// details | 👀 Full file preview + +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + +```Python +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001.py!} +``` + +//// + +/// + +With this configuration, when we delete a team, SQLModel (actually SQLAlchemy) will: + +* Make sure the objects for the **related records are loaded**, in this case, the `heroes`. If they are not loaded, it will send a `SELECT` query to the database to get them. +* Send a `DELETE` query to the database **including each related record** (each hero). +* Finally, **delete the initial record** (the team) with another `DELETE` query. + +This way, the internal **Python code** will take care of deleting the related records, by emitting the necessary SQL queries for each of them. + +/// tip + +The `cascade_delete` parameter is set in the `Relationship()`, on the model that **doesn't have a foreign key**. + +/// + +/// note | Technical Details + +Setting `cascade_delete=True` in the `Relationship()` will configure SQLAlchemy to use `cascade="all, delete-orphan"`, which is the most common and useful configuration when wanting to cascade deletes. + +You can read more about it in the SQLAlchemy docs. + +/// + +## Delete in the Database with `ondelete` + +In the previous section we saw that using `cascade_delete` handles automatic deletions from the Python code. + +But what happens if someone **interacts with the database directly**, not using our code, and **deletes a team with SQL**? + +For those cases, we can configure the database to **automatically delete** the related records with the `ondelete` parameter in `Field()`. + +### `ondelete` Options + +The `ondelete` parameter will set a SQL `ON DELETE` in the **foreign key column** in the database. + +`ondelete` can have these values: + +* `CASCADE`: **Automatically delete this record** (hero) when the related one (team) is deleted. +* `SET NULL`: Set this **foreign key** (`hero.team_id`) field to `NULL` when the related record is deleted. +* `RESTRICT`: **Prevent** the deletion of this record (hero) if there is a foreign key value by raising an error. + +## Set `ondelete` to `CASCADE` + +If we want to configure the database to **automatically delete** the related records when the parent is deleted, we can set `ondelete="CASCADE"`. + +//// tab | Python 3.10+ + +```Python hl_lines="18" +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py310.py[ln:1-19]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="21" +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py39.py[ln:1-23]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + +```Python hl_lines="21" +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001.py[ln:1-23]!} + +# Code below omitted 👇 +``` + +//// + +/// details | 👀 Full file preview + +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + +```Python +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001.py!} +``` + +//// + +/// + +Now, when we **create the tables** in the database, the `team_id` column in the `Hero` table will have an `ON DELETE CASCADE` in its definition at the database level. + +This will **configure the database** to **automatically delete** the records (heroes) when the related record (team) is deleted. + +/// tip + +The `ondelete` parameter is set in the `Field()`, on the model that **has a foreign key**. + +/// + +## Using `cascade_delete` or `ondelete` + +At this point, you might be wondering if you should use `cascade_delete` or `ondelete`. The answer is: **both**! 🤓 + +The `ondelete` will **configure the database**, in case someone interacts with it directly. + +But `cascade_delete` is still needed to tell SQLAlchemy that it should delete the **Python objects** in memory. + +### Foreign Key Constraint Support + +Some databases don't support foreign key constraints. + +For example, **SQLite** doesn't support them by default. They have to be manually enabled with a custom SQL command: + +``` +PRAGMA foreign_keys = ON; +``` + +So, in general is a good idea to have both `cascade_delete` and `ondelete` configured. + +/// tip + +You will learn more about how to **disable the default** automatic SQLModel (SQLAlchemy) behavior and **only rely on the database** down below, in the section about `passive_deletes`. + +/// + +### `cascade_delete` on `Relationship()` and `ondelete` on `Field()` + +Just a note to remember... 🤓 + +* `ondelete` is put on the `Field()` with a **foreign key**. On the **"many"** side in "one-to-many" relationships. + +```Python +class Hero(SQLModel, table=True): + ... + + team_id: int Field(foreign_key="team.id", ondelete="CASCADE") +``` + +* `cascade_delete` is put on the `Relationship()`. Normally on the **"one"** side in "one-to-many" relationships, the side **without a foreign key**. + +```Python +class Team(SQLModel, table=True): + ... + + heroes: list[Hero] = Relationship(cascade_delete=True) +``` + +## Remove a Team and its Heroes + +Now, when we **delete a team**, we don't need to do anything else, it's **automatically** going to **delete its heroes**. + +//// tab | Python 3.10+ + +```Python hl_lines="7" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py310.py[ln:76-82]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="7" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py39.py[ln:80-86]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + +```Python hl_lines="7" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001.py[ln:80-86]!} + +# Code below omitted 👇 +``` + +//// + +/// details | 👀 Full file preview + +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + +```Python +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001.py!} +``` + +//// + +/// + +## Confirm Heroes are Deleted + +We can confirm that **after deleting the team** `Wakaland`, the heroes `Black Lion` and `Princess Sure-E` are **also deleted**. + +If we try to select them from the database, we will **no longer find them**. + +//// tab | Python 3.10+ + +```Python hl_lines="5 8 10 13" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py310.py[ln:85-95]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="5 8 10 13" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py39.py[ln:89-99]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + +```Python hl_lines="5 8 10 13" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001.py[ln:89-99]!} + +# Code below omitted 👇 +``` + +//// + +/// details | 👀 Full file preview + +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + +```Python +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001.py!} +``` + +//// + +/// + +## Run the Program with `cascade_delete=True` and `ondelete="CASCADE"` + +We can confirm everything is working by running the program. + +
+ +```console +$ python app.py + +// Some boilerplate and previous output omitted 😉 + +// The team table is created as before +CREATE TABLE team ( + id INTEGER NOT NULL, + name VARCHAR NOT NULL, + headquarters VARCHAR NOT NULL, + PRIMARY KEY (id) +) + +// The hero table is created with the ON DELETE CASCADE 🎉 +// In SQLite, it also includes REFERENCES team (id), this is needed by SQLite to work with the ON DELETE CASCADE properly. +// SQLAlchemy takes care of setting it up for us to make sure it works 🤓 +CREATE TABLE hero ( + id INTEGER NOT NULL, + name VARCHAR NOT NULL, + secret_name VARCHAR NOT NULL, + age INTEGER, + team_id INTEGER, + PRIMARY KEY (id), + FOREIGN KEY(team_id) REFERENCES team (id) ON DELETE CASCADE +) + +// We select the team Wakaland +INFO Engine SELECT team.id, team.name, team.headquarters +FROM team +WHERE team.name = ? +INFO Engine [generated in 0.00014s] ('Wakaland',) + +// Then, because of delete_cascade, right before deleting Wakaland, SQLAlchemy loads the heroes +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age, hero.team_id AS hero_team_id +FROM hero +WHERE ? = hero.team_id +INFO Engine [generated in 0.00020s] (3,) + +// Next, before deleting the Wakaland team, it sends a DELETE statement including each related hero: Black Lion and Princess Sure-E, with IDs 4 and 5 +INFO Engine DELETE FROM hero WHERE hero.id = ? +INFO Engine [generated in 0.00022s] [(4,), (5,)] + +// After that, it will send the delete for the team Wakaland with ID 3 +INFO Engine DELETE FROM team WHERE team.id = ? +INFO Engine [generated in 0.00017s] (3,) + +// Print the deleted team +Deleted team: name='Wakaland' id=3 headquarters='Wakaland Capital City' + +// Finally, we try to select the heroes from Wakaland, Black Lion and Princess Sure-E and print them, but they are now deleted +Black Lion not found: None +Princess Sure-E not found: None +``` + +
+ +## `ondelete` with `SET NULL` + +We can configure the database to **set the foreign key** (the `team_id` in the `hero` table) to **`NULL`** when the related record (in the `team` table) is deleted. + +In this case, the side with `Relationship()` won't have `delete_cascade`, but the side with `Field()` and a `foreign_key` will have `ondelete="SET NULL"`. + +//// tab | Python 3.10+ + +```Python hl_lines="19" +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py310.py[ln:1-21]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="21" +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py39.py[ln:1-23]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + +```Python hl_lines="21" +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002.py[ln:1-23]!} + +# Code below omitted 👇 +``` + + +//// + +/// details | 👀 Full file preview + +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + +```Python +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002.py!} +``` + +//// + +/// + +The configuration above is setting the `team_id` column from the `Hero` table to have an `ON DELETE SET NULL`. + +This way, when someone deletes a team from the database using SQL directly, the database will go to the heroes for that team and set `team_id` to `NULL` (if the database supports it). + +/// tip + +The foreign key should allow `None` values (`NULL` in the database), otherwise you would end up having an Integrity Error by violating the `NOT NULL` constraint. + +So `team_id` needs to have a type with `None`, like: + +```Python +team_id: int | None +``` + +/// + +### Not Using `ondelete="SET NULL"` + +What happens if you don't use `ondelete="SET NULL"`, don't set anything on `cascade_delete`, and delete a team? + +The default behavior is that SQLModel (actually SQLAlchemy) will go to the heroes and set their `team_id` to `NULL` from the **Python code**. + +So, **by default**, those `team_id` fields will be **set to `NULL`**. + +But if someone goes to the database and **manually deletes a team**, the heroes could end up with a `team_id` pointing to a non-existing team. + +Adding the `ondelete="SET NULL"` configures the database itself to also set those fields to `NULL`. + +But if you delete a team from code, by default, SQLModel (actually SQLAlchemy) will update those `team_id` fields to `NULL` even before the database `SET NULL` takes effect. + +### Removing a Team with `SET NULL` + +Removing a team has the **same code** as before, the only thing that changes is the configuration underneath in the database. + +//// tab | Python 3.10+ + +```Python hl_lines="7" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py310.py[ln:78-84]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="7" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py39.py[ln:80-86]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + +```Python hl_lines="7" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002.py[ln:80-86]!} + +# Code below omitted 👇 +``` + +//// + +/// details | 👀 Full file preview + +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + +```Python +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002.py!} +``` + +//// + +/// + +The result would be these tables. + +#### Team Table after `SET NULL` + +| id | name | headquarters | +| ---- | ---------- | --------------------- | +| 1 | Z-Force | Sister Margaret's Bar | +| 2 | Preventers | Sharp Tower | + +#### Hero Table after `SET NULL` + +| id | name | secret_name | age | team_id | +| ---- | --------------- | ---------------- | ---- | ------- | +| 1 | Deadpond | Dive WIlson | | 1 | +| 2 | Rusty-Man | Tommy Sharp | 48 | 2 | +| 3 | Spider-Boy | Pedro Parqueador | | 2 | +| 4 | Black Lion | Trevor Challa | 35 | NULL | +| 5 | Princess Sure-E | Sure-E | | NULL | + +#### Visual Teams and Heroes after `SET NULL` + +We could visualize them like this: + +```mermaid +flowchart TB + subgraph "Z-Force" + d("Deadpond") + end + subgraph "Preventers" + r("Rusty-Man") + s("Spider-Boy") + end + b("Black Lion") + p("Princess Sure-E") +``` + +### Run the program with `SET NULL` + +Let's confirm it all works by running the program now: + +
+ +```console +$ python app.py + +// Some boilerplate and previous output omitted 😉 + +// The hero table is created with the ON DELETE SET NULL 🎉 +// In SQLite, it also includes: REFERENCES team (id). This REFERENCES is needed by SQLite to work with the ON DELETE CASCADE properly. +// SQLModel with SQLAlchemy takes care of setting it up for us to make sure it works 🤓 +CREATE TABLE hero ( + id INTEGER NOT NULL, + name VARCHAR NOT NULL, + secret_name VARCHAR NOT NULL, + age INTEGER, + team_id INTEGER, + PRIMARY KEY (id), + FOREIGN KEY(team_id) REFERENCES team (id) ON DELETE SET NULL +) + +// We select the team Wakaland +INFO Engine SELECT team.id, team.name, team.headquarters +FROM team +WHERE team.id = ? +INFO Engine [generated in 0.00010s] (3,) +Team Wakaland: id=3 name='Wakaland' headquarters='Wakaland Capital City' + +// Then, right before deleting Wakaland, the heroes are loaded automatically +INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age, hero.team_id AS hero_team_id +FROM hero +WHERE ? = hero.team_id +INFO Engine [generated in 0.00020s] (3,) + +// Next, before deleting the Wakaland team, it sends an UPDATE statement including each related hero: Black Lion and Princess Sure-E, with IDs 4 and 5, to set their team_id to NULL. This is not the SET NULL we added, this is just the default SQLModel (SQLAlchemy) behavior. +INFO Engine UPDATE hero SET team_id=? WHERE hero.id = ? +INFO Engine [generated in 0.00009s] [(None, 4), (None, 5)] + +// After that, it will send the delete for the team Wakaland with ID 3 +INFO Engine DELETE FROM team WHERE team.id = ? +INFO Engine [generated in 0.00017s] (3,) + +// Print the deleted team +Deleted team: name='Wakaland' id=3 headquarters='Wakaland Capital City' + +// Finally, we select the heroes Black Lion and Princess Sure-E and print them, they no longer have a team +Black Lion has no team: age=35 id=4 name='Black Lion' secret_name='Trevor Challa' team_id=None +Princess Sure-E has no team: age=None id=5 name='Princess Sure-E' secret_name='Sure-E' team_id=None +``` + +
+ +The team `Wakaland` was deleted and all of its heroes were left without a team, or in other words, with their `team_id` set to `NULL`, but still kept in the database! 🤓 + +## Let the Database Handle it with `passive_deletes` + +In the previous examples we configured `ondelete` with `CASCADE` and `SET NULL` to configure the database to handle the deletion of related records automatically. But we actually **never used that functionality** ourselves, because SQLModel (SQLAlchemy) **by default loads** the related records and **deletes** them or updates them with **NULL** before sending the `DELETE` for the team. + +If you know your database would be able to correctly handle the deletes or updates on its own, just with `ondelete="CASCADE"` or `ondelete="SET NULL"`, you can use `passive_deletes="all"` in the `Relationship()` to tell SQLModel (actually SQLAlchemy) to **not delete or update** those records (for heroes) before sending the `DELETE` for the team. + +### Enable Foreign Key Support in SQLite + +To be able to test this out with SQLite, we first need to enable foreign key support. + +//// tab | Python 3.10+ + +```Python hl_lines="6" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py310.py[ln:30-33]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="6" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py39.py[ln:32-35]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + +```Python hl_lines="6" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003.py[ln:32-35]!} + +# Code below omitted 👇 +``` + +//// + +/// details | 👀 Full file preview + +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + +```Python +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003.py!} +``` + +//// + +/// + +/// info + +You can learn more about SQLite, foreign keys, and this SQL command on the SQLAlchemy docs. + +/// + +### Use `passive_deletes="all"` + +Now let's update the table model for `Team` to use `passive_deletes="all"` in the `Relationship()` for heroes. + +We will also use `ondelete="SET NULL"` in the `Hero` model table, in the foreign key `Field()` for the `team_id` to make the database set those fields to `NULL` automatically. + +//// tab | Python 3.10+ + +```Python hl_lines="9 19" +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py310.py[ln:1-21]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="11 21" +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py39.py[ln:1-23]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + +```Python hl_lines="11 21" +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003.py[ln:1-23]!} + +# Code below omitted 👇 +``` + +//// + +/// details | 👀 Full file preview + +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + +```Python +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003.py!} +``` + +//// + +/// + +### Run the Program with `passive_deletes` + +Now, if we run the program, we will see that SQLModel (SQLAlchemy) is no longer loading and updating the heroes, it just sends the `DELETE` for the team. + +
+ +```console +$ python app.py + +// Some boilerplate and previous output omitted 😉 + +// The hero table is created with the ON DELETE SET NULL as before +CREATE TABLE hero ( + id INTEGER NOT NULL, + name VARCHAR NOT NULL, + secret_name VARCHAR NOT NULL, + age INTEGER, + team_id INTEGER, + PRIMARY KEY (id), + FOREIGN KEY(team_id) REFERENCES team (id) ON DELETE SET NULL +) + +// For SQLite, we also send the custom command to enable foreign key support +INFO Engine PRAGMA foreign_keys=ON + +// We select and print the team Wakaland +Team Wakaland: id=3 name='Wakaland' headquarters='Wakaland Capital City' + +// We won't see another SELECT for the heroes, nor an UPDATE or DELETE. SQLModel (with SQLAlchemy) won't try to load and update (or delete) the related records for heroes, it will just send the DELETE for the team right away. +INFO Engine DELETE FROM team WHERE team.id = ? +INFO Engine [generated in 0.00013s] (3,) + +// At this point, because we enabled foreign key support for SQLite, the database will take care of updating the records for heroes automatically, setting their team_id to NULL + +// Print the deleted team +Deleted team: name='Wakaland' id=3 headquarters='Wakaland Capital City' + +// Finally, we select the heroes Black Lion and Princess Sure-E and print them, they no longer have a team +Black Lion has no team: age=35 id=4 name='Black Lion' secret_name='Trevor Challa' team_id=None +Princess Sure-E has no team: age=None id=5 name='Princess Sure-E' secret_name='Sure-E' team_id=None +``` + +
+ +## `ondelete` with `RESTRICT` + +We can also configure the database to **prevent the deletion** of a record (a team) if there are related records (heroes). + +In this case, when someone attempts to **delete a team with heroes** in it, the database will **raise an error**. + +And because this is configured in the database, it will happen even if someone **interacts with the database directly using SQL** (if the database supports it). + +/// tip + +For SQLite, this also needs enabling foreign key support. + +/// + +### Enable Foreign Key Support in SQLite for `RESTRICT` + +As `ondelete="RESTRICT"` is mainly a database-level constraint, let's enable foreign key support in SQLite first to be able to test it. + +//// tab | Python 3.10+ + +```Python hl_lines="6" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py310.py[ln:30-33]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="6" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py39.py[ln:32-35]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + +```Python hl_lines="6" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004.py[ln:32-35]!} + +# Code below omitted 👇 +``` + +//// + +/// details | 👀 Full file preview + +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + +```Python +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004.py!} +``` + +//// + +/// + +### Use `ondelete="RESTRICT"` + +Let's set `ondelete="RESTRICT"` in the foreign key `Field()` for the `team_id` in the `Hero` model table. + +And in the `Team` model table, we will use `passive_deletes="all"` in the `Relationship()` for heroes, this way the default behavior of setting foreign keys from deleted models to `NULL` will be disabled, and when we try to delete a team with heroes, the database will **raise an error**. + +/// tip + +Notice that we don't set `cascade_delete` in the `Team` model table. + +/// + +//// tab | Python 3.10+ + +```Python hl_lines="9 19" +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py310.py[ln:1-21]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="11 21" +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py39.py[ln:1-23]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + +```Python hl_lines="11 21" +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004.py[ln:1-23]!} + +# Code below omitted 👇 +``` + +//// + +/// details | 👀 Full file preview + +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + +```Python +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004.py!} +``` + +//// + +/// + +### Run the Program with `RESTRICT`, See the Error + +Now, if we run the program and try to delete a team with heroes, we will see an error. + +
+ +```console +$ python app.py + +// Some boilerplate and previous output omitted 😉 + +// The hero table is created with the ON DELETE RESTRICT +CREATE TABLE hero ( + id INTEGER NOT NULL, + name VARCHAR NOT NULL, + secret_name VARCHAR NOT NULL, + age INTEGER, + team_id INTEGER, + PRIMARY KEY (id), + FOREIGN KEY(team_id) REFERENCES team (id) ON DELETE RESTRICT +) + +// Now, when we reach the point of deleting a team with heroes, we will see an error +Traceback (most recent call last): + File "/home/user/code... + +sqlite3.IntegrityError: FOREIGN KEY constraint failed + +// More error output here... + +sqlalchemy.exc.IntegrityError: (sqlite3.IntegrityError) FOREIGN KEY constraint failed +[SQL: DELETE FROM team WHERE team.id = ?] +[parameters: (3,)] +``` + +
+ +Great! The database didn't let us commit the mistake of deleting a team with heroes. 🤓 + +/// tip + +If you want to test if the `PRAGMA foreign_keys=ON` is necessary, **comment that line** and run it again, you will **not see an error**. 😱 + +The same with `passive_deletes="all"`, if you **comment that line**, SQLModel (SQLAlchemy) will load and update the heroes before deleting the team, set their foreign key `team_id` to `NULL` and **the constraint won't work as expected**, you will not see an error. 😅 + +/// + +### Update Heroes Before Deleting the Team + +After having the `ondelete="RESTRICT"` in place, SQLite configured to support foreign keys, and `passive_deletes="all"` in the `Relationship()`, if we try to delete a team with heroes, we will see an error. + +If we want to delete the team, we need to **update the heroes first** and set their `team_id` to `None` (or `NULL` in the database). + +By calling the method `.clear()` from a list, we remove all its items. So, by calling `team.heroes.clear()` and saving that to the database, we disassociate the heroes from the team, that will set their `team_id` to `None`. + +/// tip + +Calling `team.heroes.clear()` is very similar to what SQLModel (actually SQLAlchemy) would have done if we didn't have `passive_deletes="all"` configured. + +/// + +//// tab | Python 3.10+ + +```Python hl_lines="7" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005_py310.py[ln:80-88]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="7" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005_py39.py[ln:82-90]!} + +# Code below omitted 👇 +``` + +//// + +//// tab | Python 3.7+ + +```Python hl_lines="7" +# Code above omitted 👆 + +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005.py[ln:82-90]!} + +# Code below omitted 👇 +``` + +//// + +/// details | 👀 Full file preview + +//// tab | Python 3.10+ + +```Python +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005_py39.py!} +``` + +//// + +//// tab | Python 3.7+ + +```Python +{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005.py!} +``` + +//// + +/// + +### Run the Program Deleting Heroes First + +Now, if we run the program and delete the heroes first, we will be able to delete the team without any issues. + +
+ +```console +$ python app.py + +// Some boilerplate and previous output omitted 😉 + +// The hero table is created with the ON DELETE RESTRICT +CREATE TABLE hero ( + id INTEGER NOT NULL, + name VARCHAR NOT NULL, + secret_name VARCHAR NOT NULL, + age INTEGER, + team_id INTEGER, + PRIMARY KEY (id), + FOREIGN KEY(team_id) REFERENCES team (id) ON DELETE RESTRICT +) + +// We manually disassociate the heroes from the team +INFO Engine UPDATE hero SET team_id=? WHERE hero.id = ? +INFO Engine [generated in 0.00008s] [(None, 4), (None, 5)] + +// We print the team from which we removed heroes +Team with removed heroes: name='Wakaland' id=3 headquarters='Wakaland Capital City' + +// Now we can delete the team +INFO Engine DELETE FROM team WHERE team.id = ? +INFO Engine [generated in 0.00008s] (3,) +INFO Engine COMMIT +Deleted team: name='Wakaland' id=3 headquarters='Wakaland Capital City' + +// The heroes Black Lion and Princess Sure-E are no longer associated with the team +Black Lion has no team: secret_name='Trevor Challa' name='Black Lion' team_id=None age=35 id=4 +Princess Sure-E has no team: secret_name='Sure-E' name='Princess Sure-E' team_id=None age=None id=5 +``` + +
+ +## Conclusion + +In many cases, **you don't really need to configure anything**. 😎 + +In some cases, when you want to **cascade** the delete of a record to its related records automatically (delete a team with its heroes), you can: + +* Use `cascade_delete=True` in the `Relationship()` on the side **without a foreign key** +* And use `ondelete="CASCADE"` in the `Field()` with the **foreign key** + +That will **cover most of the use cases**. 🚀 + +And if you need something else, you can refer the additional options described above. 🤓 diff --git a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/__init__.py b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001.py b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001.py new file mode 100644 index 0000000000..877aeb3c67 --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001.py @@ -0,0 +1,110 @@ +from typing import List, Optional + +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: List["Hero"] = Relationship(back_populates="team", cascade_delete=True) + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + team_id: Optional[int] = Field( + default=None, foreign_key="team.id", ondelete="CASCADE" + ) + team: Optional[Team] = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + 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 = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + +def delete_team(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Wakaland") + team = session.exec(statement).one() + session.delete(team) + session.commit() + print("Deleted team:", team) + + +def select_deleted_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Black Lion") + result = session.exec(statement) + hero = result.first() + print("Black Lion not found:", hero) + + statement = select(Hero).where(Hero.name == "Princess Sure-E") + result = session.exec(statement) + hero = result.first() + print("Princess Sure-E not found:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + delete_team() + select_deleted_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py310.py b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py310.py new file mode 100644 index 0000000000..8757aaa774 --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py310.py @@ -0,0 +1,106 @@ +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team", cascade_delete=True) + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field(default=None, foreign_key="team.id", ondelete="CASCADE") + team: Team | None = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + 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 = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + +def delete_team(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Wakaland") + team = session.exec(statement).one() + session.delete(team) + session.commit() + print("Deleted team:", team) + + +def select_deleted_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Black Lion") + result = session.exec(statement) + hero = result.first() + print("Black Lion not found:", hero) + + statement = select(Hero).where(Hero.name == "Princess Sure-E") + result = session.exec(statement) + hero = result.first() + print("Princess Sure-E not found:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + delete_team() + select_deleted_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py39.py b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py39.py new file mode 100644 index 0000000000..201f07675d --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py39.py @@ -0,0 +1,110 @@ +from typing import Optional + +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team", cascade_delete=True) + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + team_id: Optional[int] = Field( + default=None, foreign_key="team.id", ondelete="CASCADE" + ) + team: Optional[Team] = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + 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 = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + +def delete_team(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Wakaland") + team = session.exec(statement).one() + session.delete(team) + session.commit() + print("Deleted team:", team) + + +def select_deleted_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Black Lion") + result = session.exec(statement) + hero = result.first() + print("Black Lion not found:", hero) + + statement = select(Hero).where(Hero.name == "Princess Sure-E") + result = session.exec(statement) + hero = result.first() + print("Princess Sure-E not found:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + delete_team() + select_deleted_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002.py b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002.py new file mode 100644 index 0000000000..ac97a20f28 --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002.py @@ -0,0 +1,110 @@ +from typing import List, Optional + +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: List["Hero"] = Relationship(back_populates="team") + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + team_id: Optional[int] = Field( + default=None, foreign_key="team.id", ondelete="SET NULL" + ) + team: Optional[Team] = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + 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 = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + +def delete_team(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Wakaland") + team = session.exec(statement).one() + session.delete(team) + session.commit() + print("Deleted team:", team) + + +def select_deleted_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Black Lion") + result = session.exec(statement) + hero = result.first() + print("Black Lion has no team:", hero) + + statement = select(Hero).where(Hero.name == "Princess Sure-E") + result = session.exec(statement) + hero = result.first() + print("Princess Sure-E has no team:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + delete_team() + select_deleted_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py310.py b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py310.py new file mode 100644 index 0000000000..be42dba697 --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py310.py @@ -0,0 +1,108 @@ +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team") + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field( + default=None, foreign_key="team.id", ondelete="SET NULL" + ) + team: Team | None = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + 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 = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + +def delete_team(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Wakaland") + team = session.exec(statement).one() + session.delete(team) + session.commit() + print("Deleted team:", team) + + +def select_deleted_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Black Lion") + result = session.exec(statement) + hero = result.first() + print("Black Lion has no team:", hero) + + statement = select(Hero).where(Hero.name == "Princess Sure-E") + result = session.exec(statement) + hero = result.first() + print("Princess Sure-E has no team:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + delete_team() + select_deleted_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py39.py b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py39.py new file mode 100644 index 0000000000..7a22f6ee16 --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py39.py @@ -0,0 +1,110 @@ +from typing import Optional + +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select + + +class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team") + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + team_id: Optional[int] = Field( + default=None, foreign_key="team.id", ondelete="SET NULL" + ) + team: Optional[Team] = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + 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 = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + +def delete_team(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Wakaland") + team = session.exec(statement).one() + session.delete(team) + session.commit() + print("Deleted team:", team) + + +def select_deleted_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Black Lion") + result = session.exec(statement) + hero = result.first() + print("Black Lion has no team:", hero) + + statement = select(Hero).where(Hero.name == "Princess Sure-E") + result = session.exec(statement) + hero = result.first() + print("Princess Sure-E has no team:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + delete_team() + select_deleted_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003.py b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003.py new file mode 100644 index 0000000000..0424f75614 --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003.py @@ -0,0 +1,112 @@ +from typing import List, Optional + +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select, text + + +class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: List["Hero"] = Relationship(back_populates="team", passive_deletes="all") + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + team_id: Optional[int] = Field( + default=None, foreign_key="team.id", ondelete="SET NULL" + ) + team: Optional[Team] = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + with engine.connect() as connection: + connection.execute(text("PRAGMA foreign_keys=ON")) # for SQLite only + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + 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 = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + +def delete_team(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Wakaland") + team = session.exec(statement).one() + session.delete(team) + session.commit() + print("Deleted team:", team) + + +def select_deleted_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Black Lion") + result = session.exec(statement) + hero = result.first() + print("Black Lion has no team:", hero) + + statement = select(Hero).where(Hero.name == "Princess Sure-E") + result = session.exec(statement) + hero = result.first() + print("Princess Sure-E has no team:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + delete_team() + select_deleted_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py310.py b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py310.py new file mode 100644 index 0000000000..4623da1ed0 --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py310.py @@ -0,0 +1,110 @@ +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select, text + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team", passive_deletes="all") + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field( + default=None, foreign_key="team.id", ondelete="SET NULL" + ) + team: Team | None = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + with engine.connect() as connection: + connection.execute(text("PRAGMA foreign_keys=ON")) # for SQLite only + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + 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 = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + +def delete_team(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Wakaland") + team = session.exec(statement).one() + session.delete(team) + session.commit() + print("Deleted team:", team) + + +def select_deleted_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Black Lion") + result = session.exec(statement) + hero = result.first() + print("Black Lion has no team:", hero) + + statement = select(Hero).where(Hero.name == "Princess Sure-E") + result = session.exec(statement) + hero = result.first() + print("Princess Sure-E has no team:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + delete_team() + select_deleted_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py39.py b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py39.py new file mode 100644 index 0000000000..0327e69f50 --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py39.py @@ -0,0 +1,112 @@ +from typing import Optional + +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select, text + + +class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team", passive_deletes="all") + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + team_id: Optional[int] = Field( + default=None, foreign_key="team.id", ondelete="SET NULL" + ) + team: Optional[Team] = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + with engine.connect() as connection: + connection.execute(text("PRAGMA foreign_keys=ON")) # for SQLite only + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + 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 = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + +def delete_team(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Wakaland") + team = session.exec(statement).one() + session.delete(team) + session.commit() + print("Deleted team:", team) + + +def select_deleted_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Black Lion") + result = session.exec(statement) + hero = result.first() + print("Black Lion has no team:", hero) + + statement = select(Hero).where(Hero.name == "Princess Sure-E") + result = session.exec(statement) + hero = result.first() + print("Princess Sure-E has no team:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + delete_team() + select_deleted_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004.py b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004.py new file mode 100644 index 0000000000..00c5ac1765 --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004.py @@ -0,0 +1,111 @@ +from typing import List, Optional + +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select, text + + +class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: List["Hero"] = Relationship(back_populates="team", passive_deletes="all") + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + team_id: Optional[int] = Field( + default=None, foreign_key="team.id", ondelete="RESTRICT" + ) + team: Optional[Team] = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + with engine.connect() as connection: + connection.execute(text("PRAGMA foreign_keys=ON")) # for SQLite only + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + 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 = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + +def delete_team(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Wakaland") + team = session.exec(statement).one() + session.delete(team) + session.commit() + print("Deleted team:", team) + + +def select_deleted_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Black Lion") + result = session.exec(statement) + hero = result.first() + print("Black Lion has no team:", hero) + + statement = select(Hero).where(Hero.name == "Princess Sure-E") + result = session.exec(statement) + hero = result.first() + print("Princess Sure-E has no team:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + delete_team() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py310.py b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py310.py new file mode 100644 index 0000000000..051c14db34 --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py310.py @@ -0,0 +1,109 @@ +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select, text + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team", passive_deletes="all") + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field( + default=None, foreign_key="team.id", ondelete="RESTRICT" + ) + team: Team | None = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + with engine.connect() as connection: + connection.execute(text("PRAGMA foreign_keys=ON")) # for SQLite only + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + 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 = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + +def delete_team(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Wakaland") + team = session.exec(statement).one() + session.delete(team) + session.commit() + print("Deleted team:", team) + + +def select_deleted_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Black Lion") + result = session.exec(statement) + hero = result.first() + print("Black Lion has no team:", hero) + + statement = select(Hero).where(Hero.name == "Princess Sure-E") + result = session.exec(statement) + hero = result.first() + print("Princess Sure-E has no team:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + delete_team() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py39.py b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py39.py new file mode 100644 index 0000000000..25badb1fdf --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py39.py @@ -0,0 +1,111 @@ +from typing import Optional + +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select, text + + +class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team", passive_deletes="all") + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + team_id: Optional[int] = Field( + default=None, foreign_key="team.id", ondelete="RESTRICT" + ) + team: Optional[Team] = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + with engine.connect() as connection: + connection.execute(text("PRAGMA foreign_keys=ON")) # for SQLite only + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + 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 = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + +def delete_team(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Wakaland") + team = session.exec(statement).one() + session.delete(team) + session.commit() + print("Deleted team:", team) + + +def select_deleted_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Black Lion") + result = session.exec(statement) + hero = result.first() + print("Black Lion has no team:", hero) + + statement = select(Hero).where(Hero.name == "Princess Sure-E") + result = session.exec(statement) + hero = result.first() + print("Princess Sure-E has no team:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + delete_team() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005.py b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005.py new file mode 100644 index 0000000000..b46a7781bc --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005.py @@ -0,0 +1,124 @@ +from typing import List, Optional + +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select, text + + +class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: List["Hero"] = Relationship(back_populates="team", passive_deletes="all") + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + team_id: Optional[int] = Field( + default=None, foreign_key="team.id", ondelete="RESTRICT" + ) + team: Optional[Team] = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + with engine.connect() as connection: + connection.execute(text("PRAGMA foreign_keys=ON")) # for SQLite only + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + 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 = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + +def remove_team_heroes(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Wakaland") + team = session.exec(statement).one() + team.heroes.clear() + session.add(team) + session.commit() + session.refresh(team) + print("Team with removed heroes:", team) + + +def delete_team(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Wakaland") + team = session.exec(statement).one() + session.delete(team) + session.commit() + print("Deleted team:", team) + + +def select_deleted_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Black Lion") + result = session.exec(statement) + hero = result.first() + print("Black Lion has no team:", hero) + + statement = select(Hero).where(Hero.name == "Princess Sure-E") + result = session.exec(statement) + hero = result.first() + print("Princess Sure-E has no team:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + remove_team_heroes() + delete_team() + select_deleted_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005_py310.py b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005_py310.py new file mode 100644 index 0000000000..1d89bcae07 --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005_py310.py @@ -0,0 +1,122 @@ +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select, text + + +class Team(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team", passive_deletes="all") + + +class Hero(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: int | None = Field(default=None, index=True) + + team_id: int | None = Field( + default=None, foreign_key="team.id", ondelete="RESTRICT" + ) + team: Team | None = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + with engine.connect() as connection: + connection.execute(text("PRAGMA foreign_keys=ON")) # for SQLite only + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + 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 = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + +def remove_team_heroes(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Wakaland") + team = session.exec(statement).one() + team.heroes.clear() + session.add(team) + session.commit() + session.refresh(team) + print("Team with removed heroes:", team) + + +def delete_team(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Wakaland") + team = session.exec(statement).one() + session.delete(team) + session.commit() + print("Deleted team:", team) + + +def select_deleted_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Black Lion") + result = session.exec(statement) + hero = result.first() + print("Black Lion has no team:", hero) + + statement = select(Hero).where(Hero.name == "Princess Sure-E") + result = session.exec(statement) + hero = result.first() + print("Princess Sure-E has no team:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + remove_team_heroes() + delete_team() + select_deleted_heroes() + + +if __name__ == "__main__": + main() diff --git a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005_py39.py b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005_py39.py new file mode 100644 index 0000000000..edc8fb0bd5 --- /dev/null +++ b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005_py39.py @@ -0,0 +1,124 @@ +from typing import Optional + +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select, text + + +class Team(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + headquarters: str + + heroes: list["Hero"] = Relationship(back_populates="team", passive_deletes="all") + + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + team_id: Optional[int] = Field( + default=None, foreign_key="team.id", ondelete="RESTRICT" + ) + team: Optional[Team] = Relationship(back_populates="heroes") + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url, echo=True) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + with engine.connect() as connection: + connection.execute(text("PRAGMA foreign_keys=ON")) # for SQLite only + + +def create_heroes(): + with Session(engine) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team=team_z_force + ) + hero_rusty_man = Hero( + name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers + ) + 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) + session.commit() + + session.refresh(hero_deadpond) + session.refresh(hero_rusty_man) + 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 = team_preventers + session.add(hero_spider_boy) + session.commit() + session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) + hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") + team_wakaland = Team( + name="Wakaland", + headquarters="Wakaland Capital City", + heroes=[hero_black_lion, hero_sure_e], + ) + session.add(team_wakaland) + session.commit() + session.refresh(team_wakaland) + print("Team Wakaland:", team_wakaland) + + +def remove_team_heroes(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Wakaland") + team = session.exec(statement).one() + team.heroes.clear() + session.add(team) + session.commit() + session.refresh(team) + print("Team with removed heroes:", team) + + +def delete_team(): + with Session(engine) as session: + statement = select(Team).where(Team.name == "Wakaland") + team = session.exec(statement).one() + session.delete(team) + session.commit() + print("Deleted team:", team) + + +def select_deleted_heroes(): + with Session(engine) as session: + statement = select(Hero).where(Hero.name == "Black Lion") + result = session.exec(statement) + hero = result.first() + print("Black Lion has no team:", hero) + + statement = select(Hero).where(Hero.name == "Princess Sure-E") + result = session.exec(statement) + hero = result.first() + print("Princess Sure-E has no team:", hero) + + +def main(): + create_db_and_tables() + create_heroes() + remove_team_heroes() + delete_team() + select_deleted_heroes() + + +if __name__ == "__main__": + main() diff --git a/mkdocs.yml b/mkdocs.yml index 09c2b7f156..b5ca6c25fa 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -74,6 +74,7 @@ nav: - tutorial/relationship-attributes/read-relationships.md - tutorial/relationship-attributes/remove-relationships.md - tutorial/relationship-attributes/back-populates.md + - tutorial/relationship-attributes/cascade-delete-relationships.md - tutorial/relationship-attributes/type-annotation-strings.md - Many to Many: - tutorial/many-to-many/index.md diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 5755bbb4fd..d8fced51fa 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -91,6 +91,7 @@ _T = TypeVar("_T") NoArgAnyCallable = Callable[[], Any] IncEx = Union[Set[int], Set[str], Dict[int, Any], Dict[str, Any], None] +OnDeleteType = Literal["CASCADE", "SET NULL", "RESTRICT"] def __dataclass_transform__( @@ -108,6 +109,7 @@ def __init__(self, default: Any = Undefined, **kwargs: Any) -> None: primary_key = kwargs.pop("primary_key", False) nullable = kwargs.pop("nullable", Undefined) foreign_key = kwargs.pop("foreign_key", Undefined) + ondelete = kwargs.pop("ondelete", Undefined) unique = kwargs.pop("unique", False) index = kwargs.pop("index", Undefined) sa_type = kwargs.pop("sa_type", Undefined) @@ -132,13 +134,17 @@ def __init__(self, default: Any = Undefined, **kwargs: Any) -> None: ) if nullable is not Undefined: raise RuntimeError( - "Passing nullable is not supported when " "also passing a sa_column" + "Passing nullable is not supported when also passing a sa_column" ) if foreign_key is not Undefined: raise RuntimeError( "Passing foreign_key is not supported when " "also passing a sa_column" ) + if ondelete is not Undefined: + raise RuntimeError( + "Passing ondelete is not supported when also passing a sa_column" + ) if unique is not Undefined: raise RuntimeError( "Passing unique is not supported when also passing a sa_column" @@ -151,10 +157,14 @@ def __init__(self, default: Any = Undefined, **kwargs: Any) -> None: raise RuntimeError( "Passing sa_type is not supported when also passing a sa_column" ) + if ondelete is not Undefined: + if foreign_key is Undefined: + raise RuntimeError("ondelete can only be used with foreign_key") super().__init__(default=default, **kwargs) self.primary_key = primary_key self.nullable = nullable self.foreign_key = foreign_key + self.ondelete = ondelete self.unique = unique self.index = index self.sa_type = sa_type @@ -168,6 +178,8 @@ def __init__( self, *, back_populates: Optional[str] = None, + cascade_delete: Optional[bool] = False, + passive_deletes: Optional[Union[bool, Literal["all"]]] = False, link_model: Optional[Any] = None, sa_relationship: Optional[RelationshipProperty] = None, # type: ignore sa_relationship_args: Optional[Sequence[Any]] = None, @@ -185,12 +197,15 @@ def __init__( "also passing a sa_relationship" ) self.back_populates = back_populates + self.cascade_delete = cascade_delete + self.passive_deletes = passive_deletes self.link_model = link_model self.sa_relationship = sa_relationship self.sa_relationship_args = sa_relationship_args self.sa_relationship_kwargs = sa_relationship_kwargs +# include sa_type, sa_column_args, sa_column_kwargs @overload def Field( default: Any = Undefined, @@ -234,6 +249,62 @@ def Field( ) -> Any: ... +# When foreign_key is str, include ondelete +# include sa_type, sa_column_args, sa_column_kwargs +@overload +def Field( + default: Any = Undefined, + *, + default_factory: Optional[NoArgAnyCallable] = None, + alias: Optional[str] = None, + title: Optional[str] = None, + description: Optional[str] = None, + exclude: Union[ + AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any + ] = None, + include: Union[ + AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any + ] = None, + const: Optional[bool] = None, + gt: Optional[float] = None, + ge: Optional[float] = None, + lt: Optional[float] = None, + le: Optional[float] = None, + multiple_of: Optional[float] = None, + max_digits: Optional[int] = None, + decimal_places: Optional[int] = None, + min_items: Optional[int] = None, + max_items: Optional[int] = None, + unique_items: Optional[bool] = None, + min_length: Optional[int] = None, + max_length: Optional[int] = None, + allow_mutation: bool = True, + regex: Optional[str] = None, + discriminator: Optional[str] = None, + repr: bool = True, + primary_key: Union[bool, UndefinedType] = Undefined, + foreign_key: str, + ondelete: Union[OnDeleteType, UndefinedType] = Undefined, + unique: Union[bool, UndefinedType] = Undefined, + nullable: Union[bool, UndefinedType] = Undefined, + index: Union[bool, UndefinedType] = Undefined, + sa_type: Union[Type[Any], UndefinedType] = Undefined, + sa_column_args: Union[Sequence[Any], UndefinedType] = Undefined, + sa_column_kwargs: Union[Mapping[str, Any], UndefinedType] = Undefined, + schema_extra: Optional[Dict[str, Any]] = None, +) -> Any: ... + + +# Include sa_column, don't include +# primary_key +# foreign_key +# ondelete +# unique +# nullable +# index +# sa_type +# sa_column_args +# sa_column_kwargs @overload def Field( default: Any = Undefined, @@ -302,6 +373,7 @@ def Field( repr: bool = True, primary_key: Union[bool, UndefinedType] = Undefined, foreign_key: Any = Undefined, + ondelete: Union[OnDeleteType, UndefinedType] = Undefined, unique: Union[bool, UndefinedType] = Undefined, nullable: Union[bool, UndefinedType] = Undefined, index: Union[bool, UndefinedType] = Undefined, @@ -339,6 +411,7 @@ def Field( repr=repr, primary_key=primary_key, foreign_key=foreign_key, + ondelete=ondelete, unique=unique, nullable=nullable, index=index, @@ -356,6 +429,8 @@ def Field( def Relationship( *, back_populates: Optional[str] = None, + cascade_delete: Optional[bool] = False, + passive_deletes: Optional[Union[bool, Literal["all"]]] = False, link_model: Optional[Any] = None, sa_relationship_args: Optional[Sequence[Any]] = None, sa_relationship_kwargs: Optional[Mapping[str, Any]] = None, @@ -366,6 +441,8 @@ def Relationship( def Relationship( *, back_populates: Optional[str] = None, + cascade_delete: Optional[bool] = False, + passive_deletes: Optional[Union[bool, Literal["all"]]] = False, link_model: Optional[Any] = None, sa_relationship: Optional[RelationshipProperty[Any]] = None, ) -> Any: ... @@ -374,6 +451,8 @@ def Relationship( def Relationship( *, back_populates: Optional[str] = None, + cascade_delete: Optional[bool] = False, + passive_deletes: Optional[Union[bool, Literal["all"]]] = False, link_model: Optional[Any] = None, sa_relationship: Optional[RelationshipProperty[Any]] = None, sa_relationship_args: Optional[Sequence[Any]] = None, @@ -381,6 +460,8 @@ def Relationship( ) -> Any: relationship_info = RelationshipInfo( back_populates=back_populates, + cascade_delete=cascade_delete, + passive_deletes=passive_deletes, link_model=link_model, sa_relationship=sa_relationship, sa_relationship_args=sa_relationship_args, @@ -531,6 +612,10 @@ def __init__( rel_kwargs: Dict[str, Any] = {} if rel_info.back_populates: rel_kwargs["back_populates"] = rel_info.back_populates + if rel_info.cascade_delete: + rel_kwargs["cascade"] = "all, delete-orphan" + if rel_info.passive_deletes: + rel_kwargs["passive_deletes"] = rel_info.passive_deletes if rel_info.link_model: ins = inspect(rel_info.link_model) local_table = getattr(ins, "local_table") # noqa: B009 @@ -642,8 +727,14 @@ def get_column_from_field(field: Any) -> Column: # type: ignore if unique is Undefined: unique = False if foreign_key: + if field_info.ondelete == "SET NULL" and not nullable: + raise RuntimeError('ondelete="SET NULL" requires nullable=True') assert isinstance(foreign_key, str) - args.append(ForeignKey(foreign_key)) + ondelete = getattr(field_info, "ondelete", Undefined) + if ondelete is Undefined: + ondelete = None + assert isinstance(ondelete, (str, type(None))) # for typing + args.append(ForeignKey(foreign_key, ondelete=ondelete)) kwargs = { "primary_key": primary_key, "nullable": nullable, diff --git a/tests/test_field_sa_column.py b/tests/test_field_sa_column.py index 7384f1fabc..e2ccc6d7ef 100644 --- a/tests/test_field_sa_column.py +++ b/tests/test_field_sa_column.py @@ -108,3 +108,14 @@ class Item(SQLModel, table=True): index=True, sa_column=Column(Integer, primary_key=True), ) + + +def test_sa_column_no_ondelete() -> None: + with pytest.raises(RuntimeError): + + class Item(SQLModel, table=True): + id: Optional[int] = Field( + default=None, + sa_column=Column(Integer, primary_key=True), + ondelete="CASCADE", + ) diff --git a/tests/test_ondelete_raises.py b/tests/test_ondelete_raises.py new file mode 100644 index 0000000000..cbcab4ca41 --- /dev/null +++ b/tests/test_ondelete_raises.py @@ -0,0 +1,37 @@ +from typing import Any, List, Union + +import pytest +from sqlmodel import Field, Relationship, SQLModel + + +def test_ondelete_requires_nullable(clear_sqlmodel: Any) -> None: + with pytest.raises(RuntimeError) as exc: + + class Team(SQLModel, table=True): + id: Union[int, None] = Field(default=None, primary_key=True) + + heroes: List["Hero"] = Relationship( + back_populates="team", passive_deletes="all" + ) + + class Hero(SQLModel, table=True): + id: Union[int, None] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Union[int, None] = Field(default=None, index=True) + + team_id: int = Field(foreign_key="team.id", ondelete="SET NULL") + team: Team = Relationship(back_populates="heroes") + + assert 'ondelete="SET NULL" requires nullable=True' in str(exc.value) + + +def test_ondelete_requires_foreign_key(clear_sqlmodel: Any) -> None: + with pytest.raises(RuntimeError) as exc: + + class Team(SQLModel, table=True): + id: Union[int, None] = Field(default=None, primary_key=True) + + age: int = Field(ondelete="CASCADE") + + assert "ondelete can only be used with foreign_key" in str(exc.value) diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/__init__.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001.py new file mode 100644 index 0000000000..863a84eb1c --- /dev/null +++ b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001.py @@ -0,0 +1,72 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ....conftest import get_testing_print_function + + +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( + tutorial001 as mod, + ) + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == [ + [ + "Created hero:", + { + "name": "Deadpond", + "secret_name": "Dive Wilson", + "team_id": 1, + "id": 1, + "age": None, + }, + ], + [ + "Created hero:", + { + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "team_id": 2, + "id": 2, + "age": 48, + }, + ], + [ + "Created hero:", + { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "team_id": None, + "id": 3, + "age": None, + }, + ], + [ + "Updated hero:", + { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "team_id": 2, + "id": 3, + "age": None, + }, + ], + [ + "Team Wakaland:", + {"name": "Wakaland", "id": 3, "headquarters": "Wakaland Capital City"}, + ], + [ + "Deleted team:", + {"name": "Wakaland", "id": 3, "headquarters": "Wakaland Capital City"}, + ], + ["Black Lion not found:", None], + ["Princess Sure-E not found:", None], + ] diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001_py310.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001_py310.py new file mode 100644 index 0000000000..3262d2b244 --- /dev/null +++ b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001_py310.py @@ -0,0 +1,73 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ....conftest import get_testing_print_function, needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( + tutorial001_py310 as mod, + ) + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == [ + [ + "Created hero:", + { + "name": "Deadpond", + "secret_name": "Dive Wilson", + "team_id": 1, + "id": 1, + "age": None, + }, + ], + [ + "Created hero:", + { + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "team_id": 2, + "id": 2, + "age": 48, + }, + ], + [ + "Created hero:", + { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "team_id": None, + "id": 3, + "age": None, + }, + ], + [ + "Updated hero:", + { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "team_id": 2, + "id": 3, + "age": None, + }, + ], + [ + "Team Wakaland:", + {"name": "Wakaland", "id": 3, "headquarters": "Wakaland Capital City"}, + ], + [ + "Deleted team:", + {"name": "Wakaland", "id": 3, "headquarters": "Wakaland Capital City"}, + ], + ["Black Lion not found:", None], + ["Princess Sure-E not found:", None], + ] diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001_py39.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001_py39.py new file mode 100644 index 0000000000..840c354e83 --- /dev/null +++ b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001_py39.py @@ -0,0 +1,73 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ....conftest import get_testing_print_function, needs_py39 + + +@needs_py39 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( + tutorial001_py39 as mod, + ) + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == [ + [ + "Created hero:", + { + "name": "Deadpond", + "secret_name": "Dive Wilson", + "team_id": 1, + "id": 1, + "age": None, + }, + ], + [ + "Created hero:", + { + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "team_id": 2, + "id": 2, + "age": 48, + }, + ], + [ + "Created hero:", + { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "team_id": None, + "id": 3, + "age": None, + }, + ], + [ + "Updated hero:", + { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "team_id": 2, + "id": 3, + "age": None, + }, + ], + [ + "Team Wakaland:", + {"name": "Wakaland", "id": 3, "headquarters": "Wakaland Capital City"}, + ], + [ + "Deleted team:", + {"name": "Wakaland", "id": 3, "headquarters": "Wakaland Capital City"}, + ], + ["Black Lion not found:", None], + ["Princess Sure-E not found:", None], + ] diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002.py new file mode 100644 index 0000000000..a7d7a26364 --- /dev/null +++ b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002.py @@ -0,0 +1,90 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ....conftest import get_testing_print_function + + +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( + tutorial002 as mod, + ) + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == [ + [ + "Created hero:", + { + "age": None, + "id": 1, + "name": "Deadpond", + "secret_name": "Dive Wilson", + "team_id": 1, + }, + ], + [ + "Created hero:", + { + "age": 48, + "id": 2, + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "team_id": 2, + }, + ], + [ + "Created hero:", + { + "age": None, + "id": 3, + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "team_id": None, + }, + ], + [ + "Updated hero:", + { + "age": None, + "id": 3, + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "team_id": 2, + }, + ], + [ + "Team Wakaland:", + {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, + ], + [ + "Deleted team:", + {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, + ], + [ + "Black Lion has no team:", + { + "age": 35, + "id": 4, + "name": "Black Lion", + "secret_name": "Trevor Challa", + "team_id": None, + }, + ], + [ + "Princess Sure-E has no team:", + { + "age": None, + "id": 5, + "name": "Princess Sure-E", + "secret_name": "Sure-E", + "team_id": None, + }, + ], + ] diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002_py310.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002_py310.py new file mode 100644 index 0000000000..5c755f3a29 --- /dev/null +++ b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002_py310.py @@ -0,0 +1,91 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ....conftest import get_testing_print_function, needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( + tutorial002_py310 as mod, + ) + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == [ + [ + "Created hero:", + { + "age": None, + "id": 1, + "name": "Deadpond", + "secret_name": "Dive Wilson", + "team_id": 1, + }, + ], + [ + "Created hero:", + { + "age": 48, + "id": 2, + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "team_id": 2, + }, + ], + [ + "Created hero:", + { + "age": None, + "id": 3, + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "team_id": None, + }, + ], + [ + "Updated hero:", + { + "age": None, + "id": 3, + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "team_id": 2, + }, + ], + [ + "Team Wakaland:", + {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, + ], + [ + "Deleted team:", + {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, + ], + [ + "Black Lion has no team:", + { + "age": 35, + "id": 4, + "name": "Black Lion", + "secret_name": "Trevor Challa", + "team_id": None, + }, + ], + [ + "Princess Sure-E has no team:", + { + "age": None, + "id": 5, + "name": "Princess Sure-E", + "secret_name": "Sure-E", + "team_id": None, + }, + ], + ] diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002_py39.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002_py39.py new file mode 100644 index 0000000000..9937f6da4c --- /dev/null +++ b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002_py39.py @@ -0,0 +1,91 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ....conftest import get_testing_print_function, needs_py39 + + +@needs_py39 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( + tutorial002_py39 as mod, + ) + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == [ + [ + "Created hero:", + { + "age": None, + "id": 1, + "name": "Deadpond", + "secret_name": "Dive Wilson", + "team_id": 1, + }, + ], + [ + "Created hero:", + { + "age": 48, + "id": 2, + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "team_id": 2, + }, + ], + [ + "Created hero:", + { + "age": None, + "id": 3, + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "team_id": None, + }, + ], + [ + "Updated hero:", + { + "age": None, + "id": 3, + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "team_id": 2, + }, + ], + [ + "Team Wakaland:", + {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, + ], + [ + "Deleted team:", + {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, + ], + [ + "Black Lion has no team:", + { + "age": 35, + "id": 4, + "name": "Black Lion", + "secret_name": "Trevor Challa", + "team_id": None, + }, + ], + [ + "Princess Sure-E has no team:", + { + "age": None, + "id": 5, + "name": "Princess Sure-E", + "secret_name": "Sure-E", + "team_id": None, + }, + ], + ] diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003.py new file mode 100644 index 0000000000..a3d3bc0f05 --- /dev/null +++ b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003.py @@ -0,0 +1,90 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from tests.conftest import get_testing_print_function + + +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( + tutorial003 as mod, + ) + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == [ + [ + "Created hero:", + { + "age": None, + "id": 1, + "name": "Deadpond", + "secret_name": "Dive Wilson", + "team_id": 1, + }, + ], + [ + "Created hero:", + { + "age": 48, + "id": 2, + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "team_id": 2, + }, + ], + [ + "Created hero:", + { + "age": None, + "id": 3, + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "team_id": None, + }, + ], + [ + "Updated hero:", + { + "age": None, + "id": 3, + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "team_id": 2, + }, + ], + [ + "Team Wakaland:", + {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, + ], + [ + "Deleted team:", + {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, + ], + [ + "Black Lion has no team:", + { + "age": 35, + "id": 4, + "name": "Black Lion", + "secret_name": "Trevor Challa", + "team_id": None, + }, + ], + [ + "Princess Sure-E has no team:", + { + "age": None, + "id": 5, + "name": "Princess Sure-E", + "secret_name": "Sure-E", + "team_id": None, + }, + ], + ] diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003_py310.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003_py310.py new file mode 100644 index 0000000000..f9975f25f7 --- /dev/null +++ b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003_py310.py @@ -0,0 +1,91 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ....conftest import get_testing_print_function, needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( + tutorial003_py310 as mod, + ) + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == [ + [ + "Created hero:", + { + "age": None, + "id": 1, + "name": "Deadpond", + "secret_name": "Dive Wilson", + "team_id": 1, + }, + ], + [ + "Created hero:", + { + "age": 48, + "id": 2, + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "team_id": 2, + }, + ], + [ + "Created hero:", + { + "age": None, + "id": 3, + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "team_id": None, + }, + ], + [ + "Updated hero:", + { + "age": None, + "id": 3, + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "team_id": 2, + }, + ], + [ + "Team Wakaland:", + {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, + ], + [ + "Deleted team:", + {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, + ], + [ + "Black Lion has no team:", + { + "age": 35, + "id": 4, + "name": "Black Lion", + "secret_name": "Trevor Challa", + "team_id": None, + }, + ], + [ + "Princess Sure-E has no team:", + { + "age": None, + "id": 5, + "name": "Princess Sure-E", + "secret_name": "Sure-E", + "team_id": None, + }, + ], + ] diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003_py39.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003_py39.py new file mode 100644 index 0000000000..b68bc6237d --- /dev/null +++ b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003_py39.py @@ -0,0 +1,91 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from ....conftest import get_testing_print_function, needs_py39 + + +@needs_py39 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( + tutorial003_py39 as mod, + ) + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == [ + [ + "Created hero:", + { + "age": None, + "id": 1, + "name": "Deadpond", + "secret_name": "Dive Wilson", + "team_id": 1, + }, + ], + [ + "Created hero:", + { + "age": 48, + "id": 2, + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "team_id": 2, + }, + ], + [ + "Created hero:", + { + "age": None, + "id": 3, + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "team_id": None, + }, + ], + [ + "Updated hero:", + { + "age": None, + "id": 3, + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "team_id": 2, + }, + ], + [ + "Team Wakaland:", + {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, + ], + [ + "Deleted team:", + {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, + ], + [ + "Black Lion has no team:", + { + "age": 35, + "id": 4, + "name": "Black Lion", + "secret_name": "Trevor Challa", + "team_id": None, + }, + ], + [ + "Princess Sure-E has no team:", + { + "age": None, + "id": 5, + "name": "Princess Sure-E", + "secret_name": "Sure-E", + "team_id": None, + }, + ], + ] diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004.py new file mode 100644 index 0000000000..d5da12e6a5 --- /dev/null +++ b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004.py @@ -0,0 +1,106 @@ +from unittest.mock import patch + +import pytest +from sqlalchemy.exc import IntegrityError +from sqlmodel import Session, create_engine, select + +from tests.conftest import get_testing_print_function + + +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( + tutorial004 as mod, + ) + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.create_db_and_tables() + mod.create_heroes() + mod.select_deleted_heroes() + with Session(mod.engine) as session: + team = session.exec( + select(mod.Team).where(mod.Team.name == "Wakaland") + ).one() + team.heroes.clear() + session.add(team) + session.commit() + mod.delete_team() + assert calls == [ + [ + "Created hero:", + { + "age": None, + "id": 1, + "name": "Deadpond", + "secret_name": "Dive Wilson", + "team_id": 1, + }, + ], + [ + "Created hero:", + { + "age": 48, + "id": 2, + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "team_id": 2, + }, + ], + [ + "Created hero:", + { + "age": None, + "id": 3, + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "team_id": None, + }, + ], + [ + "Updated hero:", + { + "age": None, + "id": 3, + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "team_id": 2, + }, + ], + [ + "Team Wakaland:", + {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, + ], + [ + "Black Lion has no team:", + { + "age": 35, + "id": 4, + "name": "Black Lion", + "secret_name": "Trevor Challa", + "team_id": 3, + }, + ], + [ + "Princess Sure-E has no team:", + { + "age": None, + "id": 5, + "name": "Princess Sure-E", + "secret_name": "Sure-E", + "team_id": 3, + }, + ], + [ + "Deleted team:", + {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, + ], + ] + + with pytest.raises(IntegrityError) as exc: + mod.main() + assert "FOREIGN KEY constraint failed" in str(exc.value) diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004_py310.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004_py310.py new file mode 100644 index 0000000000..3ce37700cf --- /dev/null +++ b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004_py310.py @@ -0,0 +1,107 @@ +from unittest.mock import patch + +import pytest +from sqlalchemy.exc import IntegrityError +from sqlmodel import Session, create_engine, select + +from tests.conftest import get_testing_print_function, needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( + tutorial004_py310 as mod, + ) + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.create_db_and_tables() + mod.create_heroes() + mod.select_deleted_heroes() + with Session(mod.engine) as session: + team = session.exec( + select(mod.Team).where(mod.Team.name == "Wakaland") + ).one() + team.heroes.clear() + session.add(team) + session.commit() + mod.delete_team() + assert calls == [ + [ + "Created hero:", + { + "age": None, + "id": 1, + "name": "Deadpond", + "secret_name": "Dive Wilson", + "team_id": 1, + }, + ], + [ + "Created hero:", + { + "age": 48, + "id": 2, + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "team_id": 2, + }, + ], + [ + "Created hero:", + { + "age": None, + "id": 3, + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "team_id": None, + }, + ], + [ + "Updated hero:", + { + "age": None, + "id": 3, + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "team_id": 2, + }, + ], + [ + "Team Wakaland:", + {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, + ], + [ + "Black Lion has no team:", + { + "age": 35, + "id": 4, + "name": "Black Lion", + "secret_name": "Trevor Challa", + "team_id": 3, + }, + ], + [ + "Princess Sure-E has no team:", + { + "age": None, + "id": 5, + "name": "Princess Sure-E", + "secret_name": "Sure-E", + "team_id": 3, + }, + ], + [ + "Deleted team:", + {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, + ], + ] + + with pytest.raises(IntegrityError) as exc: + mod.main() + assert "FOREIGN KEY constraint failed" in str(exc.value) diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004_py39.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004_py39.py new file mode 100644 index 0000000000..1c51fc0c90 --- /dev/null +++ b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004_py39.py @@ -0,0 +1,107 @@ +from unittest.mock import patch + +import pytest +from sqlalchemy.exc import IntegrityError +from sqlmodel import Session, create_engine, select + +from tests.conftest import get_testing_print_function, needs_py39 + + +@needs_py39 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( + tutorial004_py39 as mod, + ) + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.create_db_and_tables() + mod.create_heroes() + mod.select_deleted_heroes() + with Session(mod.engine) as session: + team = session.exec( + select(mod.Team).where(mod.Team.name == "Wakaland") + ).one() + team.heroes.clear() + session.add(team) + session.commit() + mod.delete_team() + assert calls == [ + [ + "Created hero:", + { + "age": None, + "id": 1, + "name": "Deadpond", + "secret_name": "Dive Wilson", + "team_id": 1, + }, + ], + [ + "Created hero:", + { + "age": 48, + "id": 2, + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "team_id": 2, + }, + ], + [ + "Created hero:", + { + "age": None, + "id": 3, + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "team_id": None, + }, + ], + [ + "Updated hero:", + { + "age": None, + "id": 3, + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "team_id": 2, + }, + ], + [ + "Team Wakaland:", + {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, + ], + [ + "Black Lion has no team:", + { + "age": 35, + "id": 4, + "name": "Black Lion", + "secret_name": "Trevor Challa", + "team_id": 3, + }, + ], + [ + "Princess Sure-E has no team:", + { + "age": None, + "id": 5, + "name": "Princess Sure-E", + "secret_name": "Sure-E", + "team_id": 3, + }, + ], + [ + "Deleted team:", + {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, + ], + ] + + with pytest.raises(IntegrityError) as exc: + mod.main() + assert "FOREIGN KEY constraint failed" in str(exc.value) diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005.py new file mode 100644 index 0000000000..a6a00608a9 --- /dev/null +++ b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005.py @@ -0,0 +1,94 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from tests.conftest import get_testing_print_function + + +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( + tutorial005 as mod, + ) + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == [ + [ + "Created hero:", + { + "name": "Deadpond", + "secret_name": "Dive Wilson", + "team_id": 1, + "id": 1, + "age": None, + }, + ], + [ + "Created hero:", + { + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "team_id": 2, + "id": 2, + "age": 48, + }, + ], + [ + "Created hero:", + { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "team_id": None, + "id": 3, + "age": None, + }, + ], + [ + "Updated hero:", + { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "team_id": 2, + "id": 3, + "age": None, + }, + ], + [ + "Team Wakaland:", + {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, + ], + [ + "Team with removed heroes:", + {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, + ], + [ + "Deleted team:", + {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, + ], + [ + "Black Lion has no team:", + { + "name": "Black Lion", + "secret_name": "Trevor Challa", + "team_id": None, + "id": 4, + "age": 35, + }, + ], + [ + "Princess Sure-E has no team:", + { + "name": "Princess Sure-E", + "secret_name": "Sure-E", + "team_id": None, + "id": 5, + "age": None, + }, + ], + ] diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005_py310.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005_py310.py new file mode 100644 index 0000000000..54ad1b79de --- /dev/null +++ b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005_py310.py @@ -0,0 +1,95 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from tests.conftest import get_testing_print_function, needs_py310 + + +@needs_py310 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( + tutorial005_py310 as mod, + ) + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == [ + [ + "Created hero:", + { + "name": "Deadpond", + "secret_name": "Dive Wilson", + "team_id": 1, + "id": 1, + "age": None, + }, + ], + [ + "Created hero:", + { + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "team_id": 2, + "id": 2, + "age": 48, + }, + ], + [ + "Created hero:", + { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "team_id": None, + "id": 3, + "age": None, + }, + ], + [ + "Updated hero:", + { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "team_id": 2, + "id": 3, + "age": None, + }, + ], + [ + "Team Wakaland:", + {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, + ], + [ + "Team with removed heroes:", + {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, + ], + [ + "Deleted team:", + {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, + ], + [ + "Black Lion has no team:", + { + "name": "Black Lion", + "secret_name": "Trevor Challa", + "team_id": None, + "id": 4, + "age": 35, + }, + ], + [ + "Princess Sure-E has no team:", + { + "name": "Princess Sure-E", + "secret_name": "Sure-E", + "team_id": None, + "id": 5, + "age": None, + }, + ], + ] diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005_py39.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005_py39.py new file mode 100644 index 0000000000..8151ab9232 --- /dev/null +++ b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005_py39.py @@ -0,0 +1,95 @@ +from unittest.mock import patch + +from sqlmodel import create_engine + +from tests.conftest import get_testing_print_function, needs_py39 + + +@needs_py39 +def test_tutorial(clear_sqlmodel): + from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( + tutorial005_py39 as mod, + ) + + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + calls = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + mod.main() + assert calls == [ + [ + "Created hero:", + { + "name": "Deadpond", + "secret_name": "Dive Wilson", + "team_id": 1, + "id": 1, + "age": None, + }, + ], + [ + "Created hero:", + { + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "team_id": 2, + "id": 2, + "age": 48, + }, + ], + [ + "Created hero:", + { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "team_id": None, + "id": 3, + "age": None, + }, + ], + [ + "Updated hero:", + { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "team_id": 2, + "id": 3, + "age": None, + }, + ], + [ + "Team Wakaland:", + {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, + ], + [ + "Team with removed heroes:", + {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, + ], + [ + "Deleted team:", + {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, + ], + [ + "Black Lion has no team:", + { + "name": "Black Lion", + "secret_name": "Trevor Challa", + "team_id": None, + "id": 4, + "age": 35, + }, + ], + [ + "Princess Sure-E has no team:", + { + "name": "Princess Sure-E", + "secret_name": "Sure-E", + "team_id": None, + "id": 5, + "age": None, + }, + ], + ] From 49735a1c93e4f4b0e5ba2acec7803ed09abe6dd7 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 21 Jul 2024 02:08:28 +0000 Subject: [PATCH 422/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 7184e98db3..201cb6298c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Features + +* ✨ Add support for cascade delete relationships: `cascade_delete`, `ondelete`, and `passive_deletes`. PR [#983](https://github.com/tiangolo/sqlmodel/pull/983) by [@estebanx64](https://github.com/estebanx64). + ### Docs * 📝 Update docs . PR [#1003](https://github.com/tiangolo/sqlmodel/pull/1003) by [@alejsdev](https://github.com/alejsdev). From 458e088170813957fd7a641fee76d06ee0bf0a03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 20 Jul 2024 21:10:30 -0500 Subject: [PATCH 423/906] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.0.?= =?UTF-8?q?21?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 2 ++ sqlmodel/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 201cb6298c..25a27b49c5 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest Changes +## 0.0.21 + ### Features * ✨ Add support for cascade delete relationships: `cascade_delete`, `ondelete`, and `passive_deletes`. PR [#983](https://github.com/tiangolo/sqlmodel/pull/983) by [@estebanx64](https://github.com/estebanx64). diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index 438d4ba07e..b02ddc9aa1 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.0.20" +__version__ = "0.0.21" # Re-export from SQLAlchemy from sqlalchemy.engine import create_engine as create_engine From f3f31a96986da80a602e9e9dfc848d546fdac061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 20 Jul 2024 21:12:48 -0500 Subject: [PATCH 424/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 25a27b49c5..35fab31f5f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,6 +7,7 @@ ### Features * ✨ Add support for cascade delete relationships: `cascade_delete`, `ondelete`, and `passive_deletes`. PR [#983](https://github.com/tiangolo/sqlmodel/pull/983) by [@estebanx64](https://github.com/estebanx64). + * New docs at: [Cascade Delete Relationships](https://sqlmodel.tiangolo.com/tutorial/relationship-attributes/cascade-delete-relationships/). ### Docs From 65d06eaa6a6f347fbde3018a1e67e20ad913b818 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 20 Jul 2024 21:14:00 -0500 Subject: [PATCH 425/906] =?UTF-8?q?=F0=9F=93=9D=20Tweak=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 35fab31f5f..9dbefd0c00 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,7 +6,7 @@ ### Features -* ✨ Add support for cascade delete relationships: `cascade_delete`, `ondelete`, and `passive_deletes`. PR [#983](https://github.com/tiangolo/sqlmodel/pull/983) by [@estebanx64](https://github.com/estebanx64). +* ✨ Add support for cascade delete relationships: `cascade_delete`, `ondelete`, and `passive_deletes`. Initial PR [#983](https://github.com/tiangolo/sqlmodel/pull/983) by [@estebanx64](https://github.com/estebanx64). * New docs at: [Cascade Delete Relationships](https://sqlmodel.tiangolo.com/tutorial/relationship-attributes/cascade-delete-relationships/). ### Docs From 605bc8229c8f3019cab4fb7ca41eb1a844788ca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 22 Jul 2024 17:52:55 -0500 Subject: [PATCH 426/906] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo=20in=20?= =?UTF-8?q?`cascade=5Fdelete`=20docs=20(#1030)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../relationship-attributes/cascade-delete-relationships.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/relationship-attributes/cascade-delete-relationships.md b/docs/tutorial/relationship-attributes/cascade-delete-relationships.md index 769d94cf99..1604b9780b 100644 --- a/docs/tutorial/relationship-attributes/cascade-delete-relationships.md +++ b/docs/tutorial/relationship-attributes/cascade-delete-relationships.md @@ -503,7 +503,7 @@ FROM team WHERE team.name = ? INFO Engine [generated in 0.00014s] ('Wakaland',) -// Then, because of delete_cascade, right before deleting Wakaland, SQLAlchemy loads the heroes +// Then, because of cascade_delete, right before deleting Wakaland, SQLAlchemy loads the heroes INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age, hero.team_id AS hero_team_id FROM hero WHERE ? = hero.team_id @@ -531,7 +531,7 @@ Princess Sure-E not found: None We can configure the database to **set the foreign key** (the `team_id` in the `hero` table) to **`NULL`** when the related record (in the `team` table) is deleted. -In this case, the side with `Relationship()` won't have `delete_cascade`, but the side with `Field()` and a `foreign_key` will have `ondelete="SET NULL"`. +In this case, the side with `Relationship()` won't have `cascade_delete`, but the side with `Field()` and a `foreign_key` will have `ondelete="SET NULL"`. //// tab | Python 3.10+ From d7af50c184238f6fa0484beb9e275e91a22755e5 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 22 Jul 2024 22:53:13 +0000 Subject: [PATCH 427/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 9dbefd0c00..7dfac822d5 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Docs + +* ✏️ Fix typo in `cascade_delete` docs. PR [#1030](https://github.com/tiangolo/sqlmodel/pull/1030) by [@tiangolo](https://github.com/tiangolo). + ## 0.0.21 ### Features From 77374aa016c81d97d2703179b6efb65b91d19c7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 31 Jul 2024 19:05:06 -0500 Subject: [PATCH 428/906] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20Deploy?= =?UTF-8?q?=20Docs=20GitHub=20Action=20to=20be=20a=20script=20and=20update?= =?UTF-8?q?=20token=20preparing=20for=20org=20(#1039)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment-docs-preview-in-pr/Dockerfile | 7 -- .../comment-docs-preview-in-pr/action.yml | 13 ---- .../comment-docs-preview-in-pr/app/main.py | 68 ------------------- .github/workflows/deploy-docs.yml | 26 +++++-- requirements-github-actions.txt | 4 ++ scripts/comment_docs_deploy_url_in_pr.py | 31 +++++++++ 6 files changed, 57 insertions(+), 92 deletions(-) delete mode 100644 .github/actions/comment-docs-preview-in-pr/Dockerfile delete mode 100644 .github/actions/comment-docs-preview-in-pr/action.yml delete mode 100644 .github/actions/comment-docs-preview-in-pr/app/main.py create mode 100644 requirements-github-actions.txt create mode 100644 scripts/comment_docs_deploy_url_in_pr.py diff --git a/.github/actions/comment-docs-preview-in-pr/Dockerfile b/.github/actions/comment-docs-preview-in-pr/Dockerfile deleted file mode 100644 index 4f20c5f10b..0000000000 --- a/.github/actions/comment-docs-preview-in-pr/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM python:3.7 - -RUN pip install httpx "pydantic==1.5.1" pygithub - -COPY ./app /app - -CMD ["python", "/app/main.py"] diff --git a/.github/actions/comment-docs-preview-in-pr/action.yml b/.github/actions/comment-docs-preview-in-pr/action.yml deleted file mode 100644 index 0eb64402d2..0000000000 --- a/.github/actions/comment-docs-preview-in-pr/action.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: Comment Docs Preview in PR -description: Comment with the docs URL preview in the PR -author: Sebastián Ramírez -inputs: - token: - description: Token for the repo. Can be passed in using {{ secrets.GITHUB_TOKEN }} - required: true - deploy_url: - description: The deployment URL to comment in the PR - required: true -runs: - using: docker - image: Dockerfile diff --git a/.github/actions/comment-docs-preview-in-pr/app/main.py b/.github/actions/comment-docs-preview-in-pr/app/main.py deleted file mode 100644 index c9fb7cbbef..0000000000 --- a/.github/actions/comment-docs-preview-in-pr/app/main.py +++ /dev/null @@ -1,68 +0,0 @@ -import logging -import sys -from pathlib import Path -from typing import Optional - -import httpx -from github import Github -from github.PullRequest import PullRequest -from pydantic import BaseModel, BaseSettings, SecretStr, ValidationError - -github_api = "https://api.github.com" - - -class Settings(BaseSettings): - github_repository: str - github_event_path: Path - github_event_name: Optional[str] = None - input_token: SecretStr - input_deploy_url: str - - -class PartialGithubEventHeadCommit(BaseModel): - id: str - - -class PartialGithubEventWorkflowRun(BaseModel): - head_commit: PartialGithubEventHeadCommit - - -class PartialGithubEvent(BaseModel): - workflow_run: PartialGithubEventWorkflowRun - - -if __name__ == "__main__": - logging.basicConfig(level=logging.INFO) - settings = Settings() - logging.info(f"Using config: {settings.json()}") - g = Github(settings.input_token.get_secret_value()) - repo = g.get_repo(settings.github_repository) - try: - event = PartialGithubEvent.parse_file(settings.github_event_path) - except ValidationError as e: - logging.error(f"Error parsing event file: {e.errors()}") - sys.exit(0) - use_pr: Optional[PullRequest] = None - for pr in repo.get_pulls(): - if pr.head.sha == event.workflow_run.head_commit.id: - use_pr = pr - break - if not use_pr: - logging.error(f"No PR found for hash: {event.workflow_run.head_commit.id}") - sys.exit(0) - github_headers = { - "Authorization": f"token {settings.input_token.get_secret_value()}" - } - url = f"{github_api}/repos/{settings.github_repository}/issues/{use_pr.number}/comments" - logging.info(f"Using comments URL: {url}") - response = httpx.post( - url, - headers=github_headers, - json={ - "body": f"📝 Docs preview for commit {use_pr.head.sha} at: {settings.input_deploy_url}" - }, - ) - if not (200 <= response.status_code <= 300): - logging.error(f"Error posting comment: {response.text}") - sys.exit(1) - logging.info("Finished") diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 41320b57b1..1984643ddd 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -6,6 +6,11 @@ on: types: - completed +permissions: + deployments: write + issues: write + pull-requests: write + jobs: deploy-docs: runs-on: ubuntu-latest @@ -38,9 +43,22 @@ jobs: directory: './site' gitHubToken: ${{ secrets.GITHUB_TOKEN }} branch: ${{ ( github.event.workflow_run.head_repository.full_name == github.repository && github.event.workflow_run.head_branch == 'main' && 'main' ) || ( github.event.workflow_run.head_sha ) }} + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + - uses: actions/cache@v4 + id: cache + with: + path: ${{ env.pythonLocation }} + key: ${{ runner.os }}-python-github-actions-${{ env.pythonLocation }}-${{ hashFiles('requirements-github-actions.txt') }}-v01 + - name: Install GitHub Actions dependencies + if: steps.cache.outputs.cache-hit != 'true' + run: pip install -r requirements-github-actions.txt - name: Comment Deploy if: steps.deploy.outputs.url != '' - uses: ./.github/actions/comment-docs-preview-in-pr - with: - token: ${{ secrets.GITHUB_TOKEN }} - deploy_url: "${{ steps.deploy.outputs.url }}" + run: python ./scripts/comment_docs_deploy_url_in_pr.py + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DEPLOY_URL: ${{ steps.deploy.outputs.url }} + COMMIT_SHA: ${{ github.event.workflow_run.head_sha }} diff --git a/requirements-github-actions.txt b/requirements-github-actions.txt new file mode 100644 index 0000000000..559dc06fb2 --- /dev/null +++ b/requirements-github-actions.txt @@ -0,0 +1,4 @@ +PyGithub>=2.3.0,<3.0.0 +pydantic>=2.5.3,<3.0.0 +pydantic-settings>=2.1.0,<3.0.0 +httpx>=0.27.0,<0.28.0 diff --git a/scripts/comment_docs_deploy_url_in_pr.py b/scripts/comment_docs_deploy_url_in_pr.py new file mode 100644 index 0000000000..3148a3bb40 --- /dev/null +++ b/scripts/comment_docs_deploy_url_in_pr.py @@ -0,0 +1,31 @@ +import logging +import sys + +from github import Github +from pydantic import SecretStr +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + github_repository: str + github_token: SecretStr + deploy_url: str + commit_sha: str + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + settings = Settings() + logging.info(f"Using config: {settings.model_dump_json()}") + g = Github(settings.github_token.get_secret_value()) + repo = g.get_repo(settings.github_repository) + use_pr = next( + (pr for pr in repo.get_pulls() if pr.head.sha == settings.commit_sha), None + ) + if not use_pr: + logging.error(f"No PR found for hash: {settings.commit_sha}") + sys.exit(0) + use_pr.as_issue().create_comment( + f"📝 Docs preview for commit {settings.commit_sha} at: {settings.deploy_url}" + ) + logging.info("Finished") From 613722df0f976c30f745238786b01ff3fab6f74b Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 1 Aug 2024 00:05:23 +0000 Subject: [PATCH 429/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 7dfac822d5..9adb18b521 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,6 +6,10 @@ * ✏️ Fix typo in `cascade_delete` docs. PR [#1030](https://github.com/tiangolo/sqlmodel/pull/1030) by [@tiangolo](https://github.com/tiangolo). +### Internal + +* ♻️ Refactor Deploy Docs GitHub Action to be a script and update token preparing for org. PR [#1039](https://github.com/tiangolo/sqlmodel/pull/1039) by [@tiangolo](https://github.com/tiangolo). + ## 0.0.21 ### Features From 839ca134bcef296cbe61094b0364d4b1290ffe3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 31 Jul 2024 20:48:06 -0500 Subject: [PATCH 430/906] =?UTF-8?q?=F0=9F=91=B7=20Update=20issue-manager.y?= =?UTF-8?q?ml=20GitHub=20Action=20permissions=20(#1040)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/issue-manager.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml index 68f1391f25..d25be4bfb2 100644 --- a/.github/workflows/issue-manager.yml +++ b/.github/workflows/issue-manager.yml @@ -14,6 +14,9 @@ on: - labeled workflow_dispatch: +permissions: + issues: write + jobs: issue-manager: runs-on: ubuntu-latest From 5e7f84ce7187e7950d204f4bba4090636083501c Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 1 Aug 2024 01:49:39 +0000 Subject: [PATCH 431/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 9adb18b521..104e622b7b 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* 👷 Update issue-manager.yml GitHub Action permissions. PR [#1040](https://github.com/tiangolo/sqlmodel/pull/1040) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor Deploy Docs GitHub Action to be a script and update token preparing for org. PR [#1039](https://github.com/tiangolo/sqlmodel/pull/1039) by [@tiangolo](https://github.com/tiangolo). ## 0.0.21 From 8c034eb7f51c881228f80c216e7cf787036cceab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 3 Aug 2024 14:13:18 -0500 Subject: [PATCH 432/906] =?UTF-8?q?=F0=9F=91=B7=20Update=20issue-manager?= =?UTF-8?q?=20(#1045)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/issue-manager.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml index d25be4bfb2..4e1a5f7ed2 100644 --- a/.github/workflows/issue-manager.yml +++ b/.github/workflows/issue-manager.yml @@ -2,7 +2,7 @@ name: Issue Manager on: schedule: - - cron: "0 0 * * *" + - cron: "11 4 * * *" issue_comment: types: - created @@ -19,8 +19,13 @@ permissions: jobs: issue-manager: + if: github.repository_owner == 'tiangolo' runs-on: ubuntu-latest steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" - uses: tiangolo/issue-manager@0.5.0 with: token: ${{ secrets.GITHUB_TOKEN }} @@ -29,5 +34,9 @@ jobs: "answered": { "delay": 864000, "message": "Assuming the original need was handled, this will be automatically closed now. But feel free to add more comments or create new issues or PRs." + }, + "changes-requested": { + "delay": 2628000, + "message": "As this PR had requested changes to be applied but has been inactive for a while, it's now going to be closed. But if there's anyone interested, feel free to create a new PR." } } From 0f8ba10a2ecb556ff6b89de14acb5f854cedda06 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 3 Aug 2024 19:13:34 +0000 Subject: [PATCH 433/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 104e622b7b..0d89834166 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* 👷 Update issue-manager. PR [#1045](https://github.com/tiangolo/sqlmodel/pull/1045) by [@tiangolo](https://github.com/tiangolo). * 👷 Update issue-manager.yml GitHub Action permissions. PR [#1040](https://github.com/tiangolo/sqlmodel/pull/1040) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor Deploy Docs GitHub Action to be a script and update token preparing for org. PR [#1039](https://github.com/tiangolo/sqlmodel/pull/1039) by [@tiangolo](https://github.com/tiangolo). From 9b632de28a5093dca4d80a99910253ddb3821501 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 3 Aug 2024 19:55:34 -0500 Subject: [PATCH 434/906] =?UTF-8?q?=F0=9F=94=A7=20Enable=20auto=20dark=20m?= =?UTF-8?q?ode=20(#1046)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mkdocs.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index b5ca6c25fa..1bf71d7d2b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,18 +6,24 @@ theme: name: material custom_dir: docs/overrides palette: - - scheme: default + - media: "(prefers-color-scheme)" + toggle: + icon: material/lightbulb-auto + name: Switch to light mode + - media: '(prefers-color-scheme: light)' + scheme: default primary: deep purple accent: amber toggle: icon: material/lightbulb name: Switch to dark mode - - scheme: slate + - media: '(prefers-color-scheme: dark)' + scheme: slate primary: deep purple accent: amber toggle: icon: material/lightbulb-outline - name: Switch to light mode + name: Switch to system preference features: - search.suggest - search.highlight From a20a3a83541e0bf70877cf11b440dabd29585c53 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 4 Aug 2024 00:55:51 +0000 Subject: [PATCH 435/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 0d89834166..7227406104 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* 🔧 Enable auto dark mode. PR [#1046](https://github.com/tiangolo/sqlmodel/pull/1046) by [@tiangolo](https://github.com/tiangolo). * 👷 Update issue-manager. PR [#1045](https://github.com/tiangolo/sqlmodel/pull/1045) by [@tiangolo](https://github.com/tiangolo). * 👷 Update issue-manager.yml GitHub Action permissions. PR [#1040](https://github.com/tiangolo/sqlmodel/pull/1040) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor Deploy Docs GitHub Action to be a script and update token preparing for org. PR [#1039](https://github.com/tiangolo/sqlmodel/pull/1039) by [@tiangolo](https://github.com/tiangolo). From ad375dcea0f68ea85f92c7f1a1b6d72d76c9bff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 8 Aug 2024 17:44:37 -0500 Subject: [PATCH 436/906] =?UTF-8?q?=F0=9F=91=B7=F0=9F=8F=BB=20Show=20docs?= =?UTF-8?q?=20deployment=20status=20and=20preview=20URLs=20in=20comment=20?= =?UTF-8?q?(#1054)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .github/workflows/deploy-docs.yml | 35 +++++++---- scripts/comment_docs_deploy_url_in_pr.py | 31 ---------- scripts/deploy_docs_status.py | 79 ++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 44 deletions(-) delete mode 100644 scripts/comment_docs_deploy_url_in_pr.py create mode 100644 scripts/deploy_docs_status.py diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 1984643ddd..619e689dcb 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -10,6 +10,7 @@ permissions: deployments: write issues: write pull-requests: write + statuses: write jobs: deploy-docs: @@ -20,6 +21,25 @@ jobs: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + - uses: actions/cache@v4 + id: cache + with: + path: ${{ env.pythonLocation }} + key: ${{ runner.os }}-python-github-actions-${{ env.pythonLocation }}-${{ hashFiles('requirements-github-actions.txt') }}-v01 + - name: Install GitHub Actions dependencies + if: steps.cache.outputs.cache-hit != 'true' + run: pip install -r requirements-github-actions.txt + - name: Deploy Docs Status Pending + run: python ./scripts/deploy_docs_status.py + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMMIT_SHA: ${{ github.event.workflow_run.head_sha }} + RUN_ID: ${{ github.run_id }} + - name: Clean site run: | rm -rf ./site @@ -43,22 +63,11 @@ jobs: directory: './site' gitHubToken: ${{ secrets.GITHUB_TOKEN }} branch: ${{ ( github.event.workflow_run.head_repository.full_name == github.repository && github.event.workflow_run.head_branch == 'main' && 'main' ) || ( github.event.workflow_run.head_sha ) }} - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - uses: actions/cache@v4 - id: cache - with: - path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-github-actions-${{ env.pythonLocation }}-${{ hashFiles('requirements-github-actions.txt') }}-v01 - - name: Install GitHub Actions dependencies - if: steps.cache.outputs.cache-hit != 'true' - run: pip install -r requirements-github-actions.txt - name: Comment Deploy if: steps.deploy.outputs.url != '' - run: python ./scripts/comment_docs_deploy_url_in_pr.py + run: python ./scripts/deploy_docs_status.py env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} DEPLOY_URL: ${{ steps.deploy.outputs.url }} COMMIT_SHA: ${{ github.event.workflow_run.head_sha }} + RUN_ID: ${{ github.run_id }} diff --git a/scripts/comment_docs_deploy_url_in_pr.py b/scripts/comment_docs_deploy_url_in_pr.py deleted file mode 100644 index 3148a3bb40..0000000000 --- a/scripts/comment_docs_deploy_url_in_pr.py +++ /dev/null @@ -1,31 +0,0 @@ -import logging -import sys - -from github import Github -from pydantic import SecretStr -from pydantic_settings import BaseSettings - - -class Settings(BaseSettings): - github_repository: str - github_token: SecretStr - deploy_url: str - commit_sha: str - - -if __name__ == "__main__": - logging.basicConfig(level=logging.INFO) - settings = Settings() - logging.info(f"Using config: {settings.model_dump_json()}") - g = Github(settings.github_token.get_secret_value()) - repo = g.get_repo(settings.github_repository) - use_pr = next( - (pr for pr in repo.get_pulls() if pr.head.sha == settings.commit_sha), None - ) - if not use_pr: - logging.error(f"No PR found for hash: {settings.commit_sha}") - sys.exit(0) - use_pr.as_issue().create_comment( - f"📝 Docs preview for commit {settings.commit_sha} at: {settings.deploy_url}" - ) - logging.info("Finished") diff --git a/scripts/deploy_docs_status.py b/scripts/deploy_docs_status.py new file mode 100644 index 0000000000..e4e665e865 --- /dev/null +++ b/scripts/deploy_docs_status.py @@ -0,0 +1,79 @@ +import logging +import re + +from github import Github +from pydantic import SecretStr +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + github_repository: str + github_token: SecretStr + deploy_url: str | None = None + commit_sha: str + run_id: int + + +def main(): + logging.basicConfig(level=logging.INFO) + settings = Settings() + + logging.info(f"Using config: {settings.model_dump_json()}") + g = Github(settings.github_token.get_secret_value()) + repo = g.get_repo(settings.github_repository) + use_pr = next( + (pr for pr in repo.get_pulls() if pr.head.sha == settings.commit_sha), None + ) + if not use_pr: + logging.error(f"No PR found for hash: {settings.commit_sha}") + return + commits = list(use_pr.get_commits()) + current_commit = [c for c in commits if c.sha == settings.commit_sha][0] + run_url = f"https://github.com/{settings.github_repository}/actions/runs/{settings.run_id}" + if not settings.deploy_url: + current_commit.create_status( + state="pending", + description="Deploy Docs", + context="deploy-docs", + target_url=run_url, + ) + logging.info("No deploy URL available yet") + return + current_commit.create_status( + state="success", + description="Deploy Docs", + context="deploy-docs", + target_url=run_url, + ) + + files = list(use_pr.get_files()) + docs_files = [f for f in files if f.filename.startswith("docs/")] + + deploy_url = settings.deploy_url.rstrip("/") + links: list[str] = [] + for f in docs_files: + match = re.match(r"docs/(.*)", f.filename) + assert match + path = match.group(1) + if path.endswith("index.md"): + path = path.replace("index.md", "") + else: + path = path.replace(".md", "/") + link = f"{deploy_url}/{path}" + links.append(link) + links.sort() + + message = f"📝 Docs preview for commit {settings.commit_sha} at: {deploy_url}" + + if links: + message += "\n\n### Modified Pages\n\n" + message += "\n".join([f"* {link}" for link in links]) + + print(message) + use_pr.as_issue().create_comment(message) + + logging.info("Finished") + + +if __name__ == "__main__": + main() From d921fb8a3ebaff4f4a177a262d29b40b83bb47c3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 8 Aug 2024 22:44:54 +0000 Subject: [PATCH 437/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 7227406104..95123cbde6 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* 👷🏻 Show docs deployment status and preview URLs in comment. PR [#1054](https://github.com/tiangolo/sqlmodel/pull/1054) by [@tiangolo](https://github.com/tiangolo). * 🔧 Enable auto dark mode. PR [#1046](https://github.com/tiangolo/sqlmodel/pull/1046) by [@tiangolo](https://github.com/tiangolo). * 👷 Update issue-manager. PR [#1045](https://github.com/tiangolo/sqlmodel/pull/1045) by [@tiangolo](https://github.com/tiangolo). * 👷 Update issue-manager.yml GitHub Action permissions. PR [#1040](https://github.com/tiangolo/sqlmodel/pull/1040) by [@tiangolo](https://github.com/tiangolo). From 756626ef5ba08ffdf36ab84ca9603558233cd334 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 8 Aug 2024 18:13:19 -0500 Subject: [PATCH 438/906] =?UTF-8?q?=F0=9F=91=B7=20Update=20docs-previews?= =?UTF-8?q?=20to=20handle=20no=20docs=20changes=20(#1056)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-docs.yml | 2 +- scripts/deploy_docs_status.py | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 619e689dcb..ca43310691 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -64,10 +64,10 @@ jobs: gitHubToken: ${{ secrets.GITHUB_TOKEN }} branch: ${{ ( github.event.workflow_run.head_repository.full_name == github.repository && github.event.workflow_run.head_branch == 'main' && 'main' ) || ( github.event.workflow_run.head_sha ) }} - name: Comment Deploy - if: steps.deploy.outputs.url != '' run: python ./scripts/deploy_docs_status.py env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} DEPLOY_URL: ${{ steps.deploy.outputs.url }} COMMIT_SHA: ${{ github.event.workflow_run.head_sha }} RUN_ID: ${{ github.run_id }} + IS_DONE: "true" diff --git a/scripts/deploy_docs_status.py b/scripts/deploy_docs_status.py index e4e665e865..8cef2f7581 100644 --- a/scripts/deploy_docs_status.py +++ b/scripts/deploy_docs_status.py @@ -12,6 +12,7 @@ class Settings(BaseSettings): deploy_url: str | None = None commit_sha: str run_id: int + is_done: bool = False def main(): @@ -30,10 +31,19 @@ def main(): commits = list(use_pr.get_commits()) current_commit = [c for c in commits if c.sha == settings.commit_sha][0] run_url = f"https://github.com/{settings.github_repository}/actions/runs/{settings.run_id}" + if settings.is_done and not settings.deploy_url: + current_commit.create_status( + state="success", + description="No Docs Changes", + context="deploy-docs", + target_url=run_url, + ) + logging.info("No docs changes found") + return if not settings.deploy_url: current_commit.create_status( state="pending", - description="Deploy Docs", + description="Deploying Docs", context="deploy-docs", target_url=run_url, ) @@ -41,7 +51,7 @@ def main(): return current_commit.create_status( state="success", - description="Deploy Docs", + description="Docs Deployed", context="deploy-docs", target_url=run_url, ) From 07d0b20c603d342cff48a471762b575137eb4e24 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 8 Aug 2024 23:13:35 +0000 Subject: [PATCH 439/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 95123cbde6..ceb9c49591 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* 👷 Update docs-previews to handle no docs changes. PR [#1056](https://github.com/tiangolo/sqlmodel/pull/1056) by [@tiangolo](https://github.com/tiangolo). * 👷🏻 Show docs deployment status and preview URLs in comment. PR [#1054](https://github.com/tiangolo/sqlmodel/pull/1054) by [@tiangolo](https://github.com/tiangolo). * 🔧 Enable auto dark mode. PR [#1046](https://github.com/tiangolo/sqlmodel/pull/1046) by [@tiangolo](https://github.com/tiangolo). * 👷 Update issue-manager. PR [#1045](https://github.com/tiangolo/sqlmodel/pull/1045) by [@tiangolo](https://github.com/tiangolo). From 9e2b5a15494eb0d14a00267bfb2ca5057cf1b702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 8 Aug 2024 18:16:56 -0500 Subject: [PATCH 440/906] =?UTF-8?q?=F0=9F=91=B7=20Add=20alls-green=20for?= =?UTF-8?q?=20test-redistribute=20(#1055)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test-redistribute.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/test-redistribute.yml b/.github/workflows/test-redistribute.yml index dc86da69c0..45e22bc4f3 100644 --- a/.github/workflows/test-redistribute.yml +++ b/.github/workflows/test-redistribute.yml @@ -51,3 +51,15 @@ jobs: run: | cd dist pip wheel --no-deps sqlmodel*.tar.gz + + # https://github.com/marketplace/actions/alls-green#why + test-redistribute-alls-green: # This job does nothing and is only used for the branch protection + if: always() + needs: + - test-redistribute + runs-on: ubuntu-latest + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} From 0678615de5bb7c04b52c44d48555564bb20ddc0f Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 8 Aug 2024 23:17:12 +0000 Subject: [PATCH 441/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index ceb9c49591..afd482e640 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* 👷 Add alls-green for test-redistribute. PR [#1055](https://github.com/tiangolo/sqlmodel/pull/1055) by [@tiangolo](https://github.com/tiangolo). * 👷 Update docs-previews to handle no docs changes. PR [#1056](https://github.com/tiangolo/sqlmodel/pull/1056) by [@tiangolo](https://github.com/tiangolo). * 👷🏻 Show docs deployment status and preview URLs in comment. PR [#1054](https://github.com/tiangolo/sqlmodel/pull/1054) by [@tiangolo](https://github.com/tiangolo). * 🔧 Enable auto dark mode. PR [#1046](https://github.com/tiangolo/sqlmodel/pull/1046) by [@tiangolo](https://github.com/tiangolo). From 970492487aa4f9fa8cfa4f381b094891f87b6a1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 8 Aug 2024 19:13:10 -0500 Subject: [PATCH 442/906] =?UTF-8?q?=F0=9F=91=B7=20Upgrade=20build=20docs?= =?UTF-8?q?=20configs=20(#1047)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .github/workflows/build-docs.yml | 12 +++-- docs/js/custom.js | 4 +- docs/tutorial/index.md | 2 + mkdocs.insiders.yml | 6 ++- mkdocs.yml | 89 +++++++++++++++++++++++++------- requirements-docs-insiders.txt | 3 ++ requirements-docs.txt | 10 ++-- scripts/docs.py | 17 ++++-- scripts/mkdocs_hooks.py | 38 ++++++++++++++ 9 files changed, 146 insertions(+), 35 deletions(-) create mode 100644 requirements-docs-insiders.txt create mode 100644 scripts/mkdocs_hooks.py diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index d4bb6cf67b..a239305286 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -28,9 +28,12 @@ jobs: - docs/** - docs_src/** - requirements-docs.txt + - requirements-docs-insiders.txt - pyproject.toml - mkdocs.yml - mkdocs.insiders.yml + - mkdocs.maybe-insiders.yml + - mkdocs.no-insiders.yml - .github/workflows/build-docs.yml - .github/workflows/deploy-docs.yml @@ -53,16 +56,15 @@ jobs: id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-docs.txt') }}-v01 + key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-docs.txt', 'requirements-docs-insiders.txt', 'requirements-docs-tests.txt') }}-v02 - name: Install docs extras if: steps.cache.outputs.cache-hit != 'true' run: pip install -r requirements-docs.txt - name: Install Material for MkDocs Insiders if: ( github.event_name != 'pull_request' || github.secret_source == 'Actions' ) && steps.cache.outputs.cache-hit != 'true' - run: | - pip install git+https://${{ secrets.SQLMODEL_MKDOCS_MATERIAL_INSIDERS }}@github.com/squidfunk/mkdocs-material-insiders.git - pip install git+https://${{ secrets.SQLMODEL_MKDOCS_MATERIAL_INSIDERS }}@github.com/pawamoy-insiders/griffe-typing-deprecated.git - pip install git+https://${{ secrets.SQLMODEL_MKDOCS_MATERIAL_INSIDERS }}@github.com/pawamoy-insiders/mkdocstrings-python.git + run: pip install -r requirements-docs-insiders.txt + env: + TOKEN: ${{ secrets.SQLMODEL_MKDOCS_MATERIAL_INSIDERS }} - uses: actions/cache@v4 with: key: mkdocs-cards-${{ github.ref }} diff --git a/docs/js/custom.js b/docs/js/custom.js index 0dda9fdd54..ef64c612a9 100644 --- a/docs/js/custom.js +++ b/docs/js/custom.js @@ -110,4 +110,6 @@ async function main() { setupTermynal() } -main() +document$.subscribe(() => { + main() +}) diff --git a/docs/tutorial/index.md b/docs/tutorial/index.md index 88d952a746..3972fc4766 100644 --- a/docs/tutorial/index.md +++ b/docs/tutorial/index.md @@ -1,5 +1,7 @@ # Intro, Installation, and First Steps +Before we start playing with **SQLModel**, let's prepare everything else we need. A bit of type annotations, setting up the environment to install everything, and installing DB Browser for SQLite. 🤓 + ## Type hints If you need a refresher about how to use Python type hints (type annotations), check FastAPI's Python types intro. diff --git a/mkdocs.insiders.yml b/mkdocs.insiders.yml index d24d754930..c1568000e2 100644 --- a/mkdocs.insiders.yml +++ b/mkdocs.insiders.yml @@ -1,3 +1,7 @@ plugins: - social: typeset: +markdown_extensions: + material.extensions.preview: + targets: + include: + - ./* diff --git a/mkdocs.yml b/mkdocs.yml index 1bf71d7d2b..df492f02b5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -25,16 +25,28 @@ theme: icon: material/lightbulb-outline name: Switch to system preference features: - - search.suggest - - search.highlight + - content.code.annotate + - content.code.copy + # - content.code.select + - content.footnote.tooltips - content.tabs.link - - navigation.indexes - content.tooltips + - navigation.footer + - navigation.indexes + - navigation.instant + - navigation.instant.prefetch + - navigation.instant.preview + - navigation.instant.progress - navigation.path - - content.code.annotate - - content.code.copy - - content.code.select # - navigation.tabs + # - navigation.tabs.sticky + - navigation.top + - navigation.tracking + - search.highlight + - search.share + - search.suggest + - toc.follow + icon: repo: fontawesome/brands/github-alt logo: img/icon-white.svg @@ -42,11 +54,14 @@ theme: language: en repo_name: tiangolo/sqlmodel repo_url: https://github.com/tiangolo/sqlmodel -edit_uri: '' plugins: - search: null - markdownextradata: - data: ./data + # Material for MkDocs + search: + social: + # Other plugins + macros: + include_yaml: + - sponsors: data/sponsors.yml nav: - SQLModel: index.md @@ -113,33 +128,68 @@ nav: - release-notes.md markdown_extensions: - markdown.extensions.attr_list: - markdown.extensions.tables: - markdown.extensions.md_in_html: + # Python Markdown + abbr: + attr_list: + footnotes: + md_in_html: + tables: toc: permalink: true + + # Python Markdown Extensions + pymdownx.betterem: + smart_enable: all + pymdownx.caret: + pymdownx.highlight: + line_spans: __span + pymdownx.inlinehilite: + pymdownx.keys: + pymdownx.mark: pymdownx.superfences: custom_fences: - name: mermaid class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' - pymdownx.betterem: - pymdownx.blocks.details: + format: !!python/name:pymdownx.superfences.fence_code_format + pymdownx.tilde: + + # pymdownx blocks pymdownx.blocks.admonition: types: - note - - info + - attention + - caution + - danger + - error - tip + - hint - warning - - danger + # Custom types + - info + pymdownx.blocks.details: pymdownx.blocks.tab: alternate_style: True + + # Other extensions mdx_include: extra: analytics: provider: google property: G-J8HVTT936W + feedback: + title: Was this page helpful? + ratings: + - icon: material/emoticon-happy-outline + name: This page was helpful + data: 1 + note: >- + Thanks for your feedback! + - icon: material/emoticon-sad-outline + name: This page could be improved + data: 0 + note: >- + Thanks for your feedback! social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/sqlmodel @@ -161,3 +211,6 @@ extra_css: extra_javascript: - js/termynal.js - js/custom.js + +hooks: + - scripts/mkdocs_hooks.py diff --git a/requirements-docs-insiders.txt b/requirements-docs-insiders.txt new file mode 100644 index 0000000000..d8d3c37a9f --- /dev/null +++ b/requirements-docs-insiders.txt @@ -0,0 +1,3 @@ +git+https://${TOKEN}@github.com/squidfunk/mkdocs-material-insiders.git@9.5.30-insiders-4.53.11 +git+https://${TOKEN}@github.com/pawamoy-insiders/griffe-typing-deprecated.git +git+https://${TOKEN}@github.com/pawamoy-insiders/mkdocstrings-python.git diff --git a/requirements-docs.txt b/requirements-docs.txt index f30c24573d..3c00f0a265 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,18 +1,18 @@ -e . -r requirements-docs-tests.txt -mkdocs-material==9.4.7 +mkdocs-material==9.5.18 mdx-include >=1.4.1,<2.0.0 -mkdocs-markdownextradata-plugin >=0.1.7,<0.3.0 mkdocs-redirects>=1.2.1,<1.3.0 pyyaml >=5.3.1,<7.0.0 # For Material for MkDocs, Chinese search -jieba==0.42.1 +# jieba==0.42.1 # For image processing by Material for MkDocs -pillow==10.1.0 +pillow==10.3.0 # For image processing by Material for MkDocs cairosvg==2.7.1 -mkdocstrings[python]==0.25.1 +# mkdocstrings[python]==0.25.1 # Enable griffe-typingdoc once dropping Python 3.7 and upgrading typing-extensions # griffe-typingdoc==0.2.5 # For griffe, it formats with black typer == 0.12.3 +mkdocs-macros-plugin==1.0.5 diff --git a/scripts/docs.py b/scripts/docs.py index cab6c87db1..8efcdda58a 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -7,9 +7,6 @@ from importlib import metadata from pathlib import Path -import mkdocs.commands.build -import mkdocs.commands.serve -import mkdocs.config import mkdocs.utils import typer from jinja2 import Template @@ -68,6 +65,13 @@ def generate_readme_content() -> str: pre_content = content[frontmatter_end:pre_end] post_content = content[post_start:] new_content = pre_content + message + post_content + # Remove content between and + new_content = re.sub( + r".*?", + "", + new_content, + flags=re.DOTALL, + ) return new_content @@ -111,8 +115,11 @@ def live() -> None: en. """ # Enable line numbers during local development to make it easier to highlight - os.environ["LINENUMS"] = "true" - mkdocs.commands.serve.serve(dev_addr="127.0.0.1:8008") + subprocess.run( + ["mkdocs", "serve", "--dev-addr", "127.0.0.1:8008", "--dirty"], + env={**os.environ, "LINENUMS": "true"}, + check=True, + ) @app.command() diff --git a/scripts/mkdocs_hooks.py b/scripts/mkdocs_hooks.py new file mode 100644 index 0000000000..f09e9a99df --- /dev/null +++ b/scripts/mkdocs_hooks.py @@ -0,0 +1,38 @@ +from typing import Any, List, Union + +from mkdocs.config.defaults import MkDocsConfig +from mkdocs.structure.files import Files +from mkdocs.structure.nav import Link, Navigation, Section +from mkdocs.structure.pages import Page + + +def generate_renamed_section_items( + items: List[Union[Page, Section, Link]], *, config: MkDocsConfig +) -> List[Union[Page, Section, Link]]: + new_items: List[Union[Page, Section, Link]] = [] + for item in items: + if isinstance(item, Section): + new_title = item.title + new_children = generate_renamed_section_items(item.children, config=config) + first_child = new_children[0] + if isinstance(first_child, Page): + if first_child.file.src_path.endswith("index.md"): + # Read the source so that the title is parsed and available + first_child.read_source(config=config) + new_title = first_child.title or new_title + # Creating a new section makes it render it collapsed by default + # no idea why, so, let's just modify the existing one + # new_section = Section(title=new_title, children=new_children) + item.title = new_title + item.children = new_children + new_items.append(item) + else: + new_items.append(item) + return new_items + + +def on_nav( + nav: Navigation, *, config: MkDocsConfig, files: Files, **kwargs: Any +) -> Navigation: + new_items = generate_renamed_section_items(nav.items, config=config) + return Navigation(items=new_items, pages=nav.pages) From ec7181a8af6069125f066a66f7acc66f223d886d Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 9 Aug 2024 00:13:28 +0000 Subject: [PATCH 443/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index afd482e640..8408d3686a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* 👷 Upgrade build docs configs. PR [#1047](https://github.com/tiangolo/sqlmodel/pull/1047) by [@tiangolo](https://github.com/tiangolo). * 👷 Add alls-green for test-redistribute. PR [#1055](https://github.com/tiangolo/sqlmodel/pull/1055) by [@tiangolo](https://github.com/tiangolo). * 👷 Update docs-previews to handle no docs changes. PR [#1056](https://github.com/tiangolo/sqlmodel/pull/1056) by [@tiangolo](https://github.com/tiangolo). * 👷🏻 Show docs deployment status and preview URLs in comment. PR [#1054](https://github.com/tiangolo/sqlmodel/pull/1054) by [@tiangolo](https://github.com/tiangolo). From e974cc4c4bbab703683e2fd14449d18fd1e702e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 8 Aug 2024 20:44:14 -0500 Subject: [PATCH 444/906] =?UTF-8?q?=F0=9F=92=84=20Update=20Termynal=20line?= =?UTF-8?q?-height=20(#1057)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 💡 💡 Add comment about custom Termynal line-height --- docs/css/termynal.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/css/termynal.css b/docs/css/termynal.css index 406c00897c..8534f91021 100644 --- a/docs/css/termynal.css +++ b/docs/css/termynal.css @@ -26,6 +26,8 @@ position: relative; -webkit-box-sizing: border-box; box-sizing: border-box; + /* Custom line-height */ + line-height: 1.2; } [data-termynal]:before { From 3e0c18440b723c9f5070230d24a9c033123f3aaf Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 9 Aug 2024 01:44:58 +0000 Subject: [PATCH 445/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 8408d3686a..a28da01353 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* 💄 Update Termynal line-height. PR [#1057](https://github.com/tiangolo/sqlmodel/pull/1057) by [@tiangolo](https://github.com/tiangolo). * 👷 Upgrade build docs configs. PR [#1047](https://github.com/tiangolo/sqlmodel/pull/1047) by [@tiangolo](https://github.com/tiangolo). * 👷 Add alls-green for test-redistribute. PR [#1055](https://github.com/tiangolo/sqlmodel/pull/1055) by [@tiangolo](https://github.com/tiangolo). * 👷 Update docs-previews to handle no docs changes. PR [#1056](https://github.com/tiangolo/sqlmodel/pull/1056) by [@tiangolo](https://github.com/tiangolo). From badebd51c1add8bf50791a087800ae84c2a9f33c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 9 Aug 2024 12:18:10 -0500 Subject: [PATCH 446/906] =?UTF-8?q?=F0=9F=94=A7=20Update=20MkDocs=20previe?= =?UTF-8?q?ws=20(#1058)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mkdocs.insiders.yml | 2 +- mkdocs.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mkdocs.insiders.yml b/mkdocs.insiders.yml index c1568000e2..80d2d4b640 100644 --- a/mkdocs.insiders.yml +++ b/mkdocs.insiders.yml @@ -4,4 +4,4 @@ markdown_extensions: material.extensions.preview: targets: include: - - ./* + - "*" diff --git a/mkdocs.yml b/mkdocs.yml index df492f02b5..3e8f2f817b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -35,7 +35,7 @@ theme: - navigation.indexes - navigation.instant - navigation.instant.prefetch - - navigation.instant.preview + # - navigation.instant.preview - navigation.instant.progress - navigation.path # - navigation.tabs From 137ed10eb0855a050e0e0a12660fa7477f48d9f6 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 9 Aug 2024 17:18:28 +0000 Subject: [PATCH 447/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index a28da01353..090e11fabf 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* 🔧 Update MkDocs previews. PR [#1058](https://github.com/tiangolo/sqlmodel/pull/1058) by [@tiangolo](https://github.com/tiangolo). * 💄 Update Termynal line-height. PR [#1057](https://github.com/tiangolo/sqlmodel/pull/1057) by [@tiangolo](https://github.com/tiangolo). * 👷 Upgrade build docs configs. PR [#1047](https://github.com/tiangolo/sqlmodel/pull/1047) by [@tiangolo](https://github.com/tiangolo). * 👷 Add alls-green for test-redistribute. PR [#1055](https://github.com/tiangolo/sqlmodel/pull/1055) by [@tiangolo](https://github.com/tiangolo). From 8c8988f33395fbf2f19f99b7e88d4d9e83176cf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 9 Aug 2024 16:27:10 -0500 Subject: [PATCH 448/906] =?UTF-8?q?=F0=9F=93=9D=20Add=20docs=20about=20rep?= =?UTF-8?q?o=20management=20and=20team=20(#1059)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/members.yml | 3 + docs/about/index.md | 3 + docs/css/custom.css | 40 ++++++++++++ docs/learn/index.md | 7 +++ docs/management-tasks.md | 115 ++++++++++++++++++++++++++++++++++ docs/management.md | 45 ++++++++++++++ docs/resources/index.md | 3 + docs/tutorial/index.md | 6 +- mkdocs.yml | 131 +++++++++++++++++++++------------------ 9 files changed, 290 insertions(+), 63 deletions(-) create mode 100644 data/members.yml create mode 100644 docs/about/index.md create mode 100644 docs/learn/index.md create mode 100644 docs/management-tasks.md create mode 100644 docs/management.md create mode 100644 docs/resources/index.md diff --git a/data/members.yml b/data/members.yml new file mode 100644 index 0000000000..062395f32f --- /dev/null +++ b/data/members.yml @@ -0,0 +1,3 @@ +members: +- login: tiangolo +- login: estebanx64 diff --git a/docs/about/index.md b/docs/about/index.md new file mode 100644 index 0000000000..53cae9270d --- /dev/null +++ b/docs/about/index.md @@ -0,0 +1,3 @@ +# About + +About **SQLModel**, its design, inspiration, and more. 🤓 diff --git a/docs/css/custom.css b/docs/css/custom.css index 65265a5389..200ac45cd6 100644 --- a/docs/css/custom.css +++ b/docs/css/custom.css @@ -29,3 +29,43 @@ a.internal-link::after { .shadow { box-shadow: 5px 5px 10px #999; } + +.user-list { + display: flex; + flex-wrap: wrap; + margin-bottom: 2rem; +} + +.user-list-center { + justify-content: space-evenly; +} + +.user { + margin: 1em; + min-width: 7em; +} + +.user .avatar-wrapper { + width: 80px; + height: 80px; + margin: 10px auto; + overflow: hidden; + border-radius: 50%; + position: relative; +} + +.user .avatar-wrapper img { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.user .title { + text-align: center; +} + +.user .count { + font-size: 80%; + text-align: center; +} diff --git a/docs/learn/index.md b/docs/learn/index.md new file mode 100644 index 0000000000..5a8253c4f6 --- /dev/null +++ b/docs/learn/index.md @@ -0,0 +1,7 @@ +# Learn + +Learn how to use **SQLModel** here. + +This includes an introduction to **databases**, **SQL**, how to interact with databases from **code** and more. + +You could consider this a **book**, a **course**, the **official** and recommended way to learn **SQLModel**. 😎 diff --git a/docs/management-tasks.md b/docs/management-tasks.md new file mode 100644 index 0000000000..14a481506b --- /dev/null +++ b/docs/management-tasks.md @@ -0,0 +1,115 @@ +# Repository Management Tasks + +These are the tasks that can be performed to manage the SQLModel repository by [team members](./management.md#team){.internal-link target=_blank}. + +/// tip + +This section is useful only to a handful of people, team members with permissions to manage the repository. You can probably skip it. 😉 + +/// + +...so, you are a [team member of SQLModel](./management.md#team){.internal-link target=_blank}? Wow, you are so cool! 😎 + +You can help with everything on [Help SQLModel - Get Help](./help.md){.internal-link target=_blank} the same ways as external contributors. But additionally, there are some tasks that only you (as part of the team) can perform. + +Here are the general instructions for the tasks you can perform. + +Thanks a lot for your help. 🙇 + +## Be Nice + +First of all, be nice. 😊 + +You probably are super nice if you were added to the team, but it's worth mentioning it. 🤓 + +### When Things are Difficult + +When things are great, everything is easier, so that doesn't need much instructions. But when things are difficult, here are some guidelines. + +Try to find the good side. In general, if people are not being unfriendly, try to thank their effort and interest, even if you disagree with the main subject (discussion, PR), just thank them for being interested in the project, or for having dedicated some time to try to do something. + +It's difficult to convey emotion in text, use emojis to help. 😅 + +In discussions and PRs, in many cases, people bring their frustration and show it without filter, in many cases exaggerating, complaining, being entitled, etc. That's really not nice, and when it happens, it lowers our priority to solve their problems. But still, try to breath, and be gentle with your answers. + +Try to avoid using bitter sarcasm or potentially passive-aggressive comments. If something is wrong, it's better to be direct (try to be gentle) than sarcastic. + +Try to be as specific and objective as possible, avoid generalizations. + +For conversations that are more difficult, for example to reject a PR, you can ask me (@tiangolo) to handle it directly. + +## Edit PR Titles + +* Edit the PR title to start with an emoji from gitmoji. + * Use the emoji character, not the GitHub code. So, use `🐛` instead of `:bug:`. This is so that it shows up correctly outside of GitHub, for example in the release notes. +* Start the title with a verb. For example `Add`, `Refactor`, `Fix`, etc. This way the title will say the action that the PR does. Like `Add support for teleporting`, instead of `Teleporting wasn't working, so this PR fixes it`. +* Edit the text of the PR title to start in "imperative", like giving an order. So, instead of `Adding support for teleporting` use `Add support for teleporting`. +* Try to make the title descriptive about what it achieves. If it's a feature, try to describe it, for example `Add support for teleporting` instead of `Create TeleportAdapter class`. +* Do not finish the title with a period (`.`). + +Once the PR is merged, a GitHub Action (latest-changes) will use the PR title to update the latest changes automatically. + +So, having a nice PR title will not only look nice in GitHub, but also in the release notes. 📝 + +## Add Labels to PRs + +The same GitHub Action latest-changes uses one label in the PR to decide the section in the release notes to put this PR in. + +Make sure you use a supported label from the latest-changes list of labels: + +* `breaking`: Breaking Changes + * Existing code will break if they update the version without changing their code. This rarely happens, so this label is not frequently used. +* `security`: Security Fixes + * This is for security fixes, like vulnerabilities. It would almost never be used. +* `feature`: Features + * New features, adding support for things that didn't exist before. +* `bug`: Fixes + * Something that was supported didn't work, and this fixes it. There are many PRs that claim to be bug fixes because the user is doing something in an unexpected way that is not supported, but they considered it what should be supported by default. Many of these are actually features or refactors. But in some cases there's an actual bug. +* `refactor`: Refactors + * This is normally for changes to the internal code that don't change the behavior. Normally it improves maintainability, or enables future features, etc. +* `upgrade`: Upgrades + * This is for upgrades to direct dependencies from the project, or extra optional dependencies, normally in `pyproject.toml`. So, things that would affect final users, they would end up receiving the upgrade in their code base once they update. But this is not for upgrades to internal dependencies used for development, testing, docs, etc. Those internal dependencies, normally in `requirements.txt` files or GitHub Action versions should be marked as `internal`, not `upgrade`. +* `docs`: Docs + * Changes in docs. This includes updating the docs, fixing typos. But it doesn't include changes to translations. + * You can normally quickly detect it by going to the "Files changed" tab in the PR and checking if the updated file(s) starts with `docs/en/docs`. The original version of the docs is always in English, so in `docs/en/docs`. +* `internal`: Internal + * Use this for changes that only affect how the repo is managed. For example upgrades to internal dependencies, changes in GitHub Actions or scripts, etc. + +/// tip + +Some tools like Dependabot, will add some labels, like `dependencies`, but have in mind that this label is not used by the `latest-changes` GitHub Action, so it won't be used in the release notes. Please make sure one of the labels above is added. + +/// + +## Review PRs + +If a PR doesn't explain what it does or why, ask for more information. + +A PR should have a specific use case that it is solving. + +* If the PR is for a feature, it should have docs. + * Unless it's a feature we want to discourage, like support for a corner case that we don't want users to use. +* The docs should include a source example file, not write Python directly in Markdown. +* If the source example(s) file can have different syntax for Python 3.8, 3.9, 3.10, there should be different versions of the file, and they should be shown in tabs in the docs. +* There should be tests testing the source example. +* Before the PR is applied, the new tests should fail. +* After applying the PR, the new tests should pass. +* Coverage should stay at 100%. +* If you see the PR makes sense, or we discussed it and considered it should be accepted, you can add commits on top of the PR to tweak it, to add docs, tests, format, refactor, remove extra files, etc. +* Feel free to comment in the PR to ask for more information, to suggest changes, etc. +* Once you think the PR is ready, move it in the internal GitHub project for me to review it. + +## Dependabot PRs + +Dependabot will create PRs to update dependencies for several things, and those PRs all look similar, but some are way more delicate than others. + +* If the PR is for a direct dependency, so, Dependabot is modifying `pyproject.toml`, **don't merge it**. 😱 Let me check it first. There's a good chance that some additional tweaks or updates are needed. +* If the PR updates one of the internal dependencies, for example it's modifying `requirements.txt` files, or GitHub Action versions, if the tests are passing, the release notes (shown in a summary in the PR) don't show any obvious potential breaking change, you can merge it. 😎 + +## Mark GitHub Discussions Answers + +When a question in GitHub Discussions has been answered, mark the answer by clicking "Mark as answer". + +Many of the current Discussion Questions were migrated from old issues. Many have the label `answered`, that means they were answered when they were issues, but now in GitHub Discussions, it's not known what is the actual response from the messages. + +You can filter discussions by `Questions` that are `Unanswered`. diff --git a/docs/management.md b/docs/management.md new file mode 100644 index 0000000000..0078e364c3 --- /dev/null +++ b/docs/management.md @@ -0,0 +1,45 @@ +# Repository Management + +Here's a short description of how the SQLModel repository is managed and maintained. + +## Owner + +I, @tiangolo, am the creator and owner of the SQLModel repository. 🤓 + +I normally give the final review to each PR before merging them. I make the final decisions on the project, I'm the BDFL. 😅 + +## Team + +There's a team of people that help manage and maintain the project. 😎 + +They have different levels of permissions and [specific instructions](./management-tasks.md){.internal-link target=_blank}. + +Some of the tasks they can perform include: + +* Adding labels to PRs. +* Editing PR titles. +* Adding commits on top of PRs to tweak them. +* Mark answers in GitHub Discussions questions, etc. +* Merge some specific types of PRs. + +Joining the team is by invitation only, and I could update or remove permissions, instructions, or membership. + +### Team Members + +This is the current list of team members. 😎 + +
+{% for user in members["members"] %} + + +{% endfor %} + +
+ +Additional to them, there's a large community of people helping each other and getting involved in the projects in different ways. + +## External Contributions + +External contributions are very welcome and appreciated, including answering questions, submitting PRs, etc. 🙇‍♂️ + +There are many ways to [help maintain SQLModel](./help.md){.internal-link target=_blank}. diff --git a/docs/resources/index.md b/docs/resources/index.md new file mode 100644 index 0000000000..d233a7833b --- /dev/null +++ b/docs/resources/index.md @@ -0,0 +1,3 @@ +# Resources + +Additional resources, how to **help** and get help, how to **contribute**, and more. ✈️ diff --git a/docs/tutorial/index.md b/docs/tutorial/index.md index 3972fc4766..0c517bbe1a 100644 --- a/docs/tutorial/index.md +++ b/docs/tutorial/index.md @@ -1,6 +1,8 @@ -# Intro, Installation, and First Steps +# Tutorial - User Guide -Before we start playing with **SQLModel**, let's prepare everything else we need. A bit of type annotations, setting up the environment to install everything, and installing DB Browser for SQLite. 🤓 +In this tutorial you will learn how to use **SQLModel**. + +But before we start playing with SQLModel, let's prepare everything else we need. A bit of type annotations, setting up the environment to install everything, and installing DB Browser for SQLite. 🤓 ## Type hints diff --git a/mkdocs.yml b/mkdocs.yml index 3e8f2f817b..0ffa87c839 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -38,8 +38,8 @@ theme: # - navigation.instant.preview - navigation.instant.progress - navigation.path - # - navigation.tabs - # - navigation.tabs.sticky + - navigation.tabs + - navigation.tabs.sticky - navigation.top - navigation.tracking - search.highlight @@ -62,69 +62,78 @@ plugins: macros: include_yaml: - sponsors: data/sponsors.yml + - members: data/members.yml nav: - SQLModel: index.md - features.md - - databases.md - - db-to-code.md - - Tutorial - User Guide: - - tutorial/index.md - - tutorial/create-db-and-table-with-db-browser.md - - tutorial/create-db-and-table.md - - tutorial/insert.md - - tutorial/automatic-id-none-refresh.md - - tutorial/select.md - - tutorial/where.md - - tutorial/indexes.md - - tutorial/one.md - - tutorial/limit-and-offset.md - - tutorial/update.md - - tutorial/delete.md - - Connect Tables - JOIN: - - tutorial/connect/index.md - - tutorial/connect/create-connected-tables.md - - tutorial/connect/create-connected-rows.md - - tutorial/connect/read-connected-data.md - - tutorial/connect/update-data-connections.md - - tutorial/connect/remove-data-connections.md - - Relationship Attributes: - - tutorial/relationship-attributes/index.md - - tutorial/relationship-attributes/define-relationships-attributes.md - - tutorial/relationship-attributes/create-and-update-relationships.md - - tutorial/relationship-attributes/read-relationships.md - - tutorial/relationship-attributes/remove-relationships.md - - tutorial/relationship-attributes/back-populates.md - - tutorial/relationship-attributes/cascade-delete-relationships.md - - tutorial/relationship-attributes/type-annotation-strings.md - - Many to Many: - - tutorial/many-to-many/index.md - - tutorial/many-to-many/create-models-with-link.md - - tutorial/many-to-many/create-data.md - - tutorial/many-to-many/update-remove-relationships.md - - tutorial/many-to-many/link-with-extra-fields.md - - tutorial/code-structure.md - - FastAPI and Pydantic: - - tutorial/fastapi/index.md - - tutorial/fastapi/simple-hero-api.md - - tutorial/fastapi/response-model.md - - tutorial/fastapi/multiple-models.md - - tutorial/fastapi/read-one.md - - tutorial/fastapi/limit-and-offset.md - - tutorial/fastapi/update.md - - tutorial/fastapi/update-extra-data.md - - tutorial/fastapi/delete.md - - tutorial/fastapi/session-with-dependency.md - - tutorial/fastapi/teams.md - - tutorial/fastapi/relationships.md - - tutorial/fastapi/tests.md - - Advanced User Guide: - - advanced/index.md - - advanced/decimal.md - - advanced/uuid.md - - alternatives.md - - help.md - - contributing.md + - Learn: + - learn/index.md + - databases.md + - db-to-code.md + - Tutorial - User Guide: + - tutorial/index.md + - tutorial/create-db-and-table-with-db-browser.md + - tutorial/create-db-and-table.md + - tutorial/insert.md + - tutorial/automatic-id-none-refresh.md + - tutorial/select.md + - tutorial/where.md + - tutorial/indexes.md + - tutorial/one.md + - tutorial/limit-and-offset.md + - tutorial/update.md + - tutorial/delete.md + - Connect Tables - JOIN: + - tutorial/connect/index.md + - tutorial/connect/create-connected-tables.md + - tutorial/connect/create-connected-rows.md + - tutorial/connect/read-connected-data.md + - tutorial/connect/update-data-connections.md + - tutorial/connect/remove-data-connections.md + - Relationship Attributes: + - tutorial/relationship-attributes/index.md + - tutorial/relationship-attributes/define-relationships-attributes.md + - tutorial/relationship-attributes/create-and-update-relationships.md + - tutorial/relationship-attributes/read-relationships.md + - tutorial/relationship-attributes/remove-relationships.md + - tutorial/relationship-attributes/back-populates.md + - tutorial/relationship-attributes/cascade-delete-relationships.md + - tutorial/relationship-attributes/type-annotation-strings.md + - Many to Many: + - tutorial/many-to-many/index.md + - tutorial/many-to-many/create-models-with-link.md + - tutorial/many-to-many/create-data.md + - tutorial/many-to-many/update-remove-relationships.md + - tutorial/many-to-many/link-with-extra-fields.md + - tutorial/code-structure.md + - FastAPI and Pydantic: + - tutorial/fastapi/index.md + - tutorial/fastapi/simple-hero-api.md + - tutorial/fastapi/response-model.md + - tutorial/fastapi/multiple-models.md + - tutorial/fastapi/read-one.md + - tutorial/fastapi/limit-and-offset.md + - tutorial/fastapi/update.md + - tutorial/fastapi/update-extra-data.md + - tutorial/fastapi/delete.md + - tutorial/fastapi/session-with-dependency.md + - tutorial/fastapi/teams.md + - tutorial/fastapi/relationships.md + - tutorial/fastapi/tests.md + - Advanced User Guide: + - advanced/index.md + - advanced/decimal.md + - advanced/uuid.md + - Resources: + - resources/index.md + - help.md + - contributing.md + - management-tasks.md + - About: + - about/index.md + - alternatives.md + - management.md - release-notes.md markdown_extensions: From 26a93e1e7d47f1a4baa43207a620feb5464de72d Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 9 Aug 2024 21:27:28 +0000 Subject: [PATCH 449/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 090e11fabf..dcf1978467 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Docs +* 📝 Add docs about repo management and team. PR [#1059](https://github.com/tiangolo/sqlmodel/pull/1059) by [@tiangolo](https://github.com/tiangolo). * ✏️ Fix typo in `cascade_delete` docs. PR [#1030](https://github.com/tiangolo/sqlmodel/pull/1030) by [@tiangolo](https://github.com/tiangolo). ### Internal From 843ed98f7666192b36bbd20d3486c9d935343dcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 9 Aug 2024 16:28:38 -0500 Subject: [PATCH 450/906] =?UTF-8?q?=F0=9F=94=A8=20Update=20docs.py=20scrip?= =?UTF-8?q?t=20to=20enable=20dirty=20reload=20conditionally=20(#1060)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/docs.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/scripts/docs.py b/scripts/docs.py index 8efcdda58a..d018ace86f 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -104,7 +104,7 @@ def verify_readme() -> None: @app.command() -def live() -> None: +def live(dirty: bool = False) -> None: """ Serve with livereload a docs site for a specific language. @@ -115,11 +115,10 @@ def live() -> None: en. """ # Enable line numbers during local development to make it easier to highlight - subprocess.run( - ["mkdocs", "serve", "--dev-addr", "127.0.0.1:8008", "--dirty"], - env={**os.environ, "LINENUMS": "true"}, - check=True, - ) + args = ["mkdocs", "serve", "--dev-addr", "127.0.0.1:8008"] + if dirty: + args.append("--dirty") + subprocess.run(args, env={**os.environ, "LINENUMS": "true"}, check=True) @app.command() From 0aa298e32e7c16251893e1f79b02238284e66787 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 9 Aug 2024 21:29:44 +0000 Subject: [PATCH 451/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index dcf1978467..004ad23d30 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* 🔨 Update docs.py script to enable dirty reload conditionally. PR [#1060](https://github.com/tiangolo/sqlmodel/pull/1060) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update MkDocs previews. PR [#1058](https://github.com/tiangolo/sqlmodel/pull/1058) by [@tiangolo](https://github.com/tiangolo). * 💄 Update Termynal line-height. PR [#1057](https://github.com/tiangolo/sqlmodel/pull/1057) by [@tiangolo](https://github.com/tiangolo). * 👷 Upgrade build docs configs. PR [#1047](https://github.com/tiangolo/sqlmodel/pull/1047) by [@tiangolo](https://github.com/tiangolo). From ea70e9f2eb609be67ad7a8f359823432dbe18684 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 9 Aug 2024 17:11:00 -0500 Subject: [PATCH 452/906] =?UTF-8?q?=F0=9F=92=84=20Add=20dark-mode=20logo?= =?UTF-8?q?=20(#1061)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +- docs/img/logo-margin/logo-margin-vector.svg | 62 ++--------- .../logo-margin/logo-margin-white-vector.svg | 100 +++++++++++++++++ docs/img/logo-margin/logo-margin-white.svg | 105 ++++++++++++++++++ docs/img/logo-margin/logo-margin.svg | 42 +------ docs/index.md | 5 +- 6 files changed, 227 insertions(+), 90 deletions(-) create mode 100644 docs/img/logo-margin/logo-margin-white-vector.svg create mode 100644 docs/img/logo-margin/logo-margin-white.svg diff --git a/README.md b/README.md index ba3bb2196e..a9e2241a86 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@

- SQLModel + SQLModel +

SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness. diff --git a/docs/img/logo-margin/logo-margin-vector.svg b/docs/img/logo-margin/logo-margin-vector.svg index 75e90c838c..335d27a6c6 100644 --- a/docs/img/logo-margin/logo-margin-vector.svg +++ b/docs/img/logo-margin/logo-margin-vector.svg @@ -1,15 +1,15 @@ + width="848.54462mm" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"> image/svg+xml - - - - - - - - - - - - + style="font-weight:bold;font-size:113.462px;line-height:1.25;font-family:Ubuntu;-inkscape-font-specification:'Ubuntu Bold';letter-spacing:0px;word-spacing:0px;fill:#7e56c2;stroke-width:2.83655" + transform="scale(1.0209259,0.97950298)" + aria-label="SQLModel" /> diff --git a/docs/img/logo-margin/logo-margin-white-vector.svg b/docs/img/logo-margin/logo-margin-white-vector.svg new file mode 100644 index 0000000000..d87b002951 --- /dev/null +++ b/docs/img/logo-margin/logo-margin-white-vector.svg @@ -0,0 +1,100 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/img/logo-margin/logo-margin-white.svg b/docs/img/logo-margin/logo-margin-white.svg new file mode 100644 index 0000000000..47ca776249 --- /dev/null +++ b/docs/img/logo-margin/logo-margin-white.svg @@ -0,0 +1,105 @@ + + + + + + + image/svg+xml + + + + + SQLModel + + + + + + + + + + + + + + + + + + + diff --git a/docs/img/logo-margin/logo-margin.svg b/docs/img/logo-margin/logo-margin.svg index 0f23f7c558..b6e4d00710 100644 --- a/docs/img/logo-margin/logo-margin.svg +++ b/docs/img/logo-margin/logo-margin.svg @@ -1,39 +1,15 @@ - + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"> image/svg+xml - - diff --git a/docs/index.md b/docs/index.md index f77cc7b8b4..6489f45a7f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,7 +3,10 @@

- SQLModel + SQLModel + + SQLModel +

SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness. From c4f935cd415b5960959b649c45264a3c8cb8abce Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 9 Aug 2024 22:11:18 +0000 Subject: [PATCH 453/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 004ad23d30..c44ea093d2 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* 💄 Add dark-mode logo. PR [#1061](https://github.com/tiangolo/sqlmodel/pull/1061) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update docs.py script to enable dirty reload conditionally. PR [#1060](https://github.com/tiangolo/sqlmodel/pull/1060) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update MkDocs previews. PR [#1058](https://github.com/tiangolo/sqlmodel/pull/1058) by [@tiangolo](https://github.com/tiangolo). * 💄 Update Termynal line-height. PR [#1057](https://github.com/tiangolo/sqlmodel/pull/1057) by [@tiangolo](https://github.com/tiangolo). From c1a2474bb0c39ff07c108d4448d1e50c6ac97252 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 12 Aug 2024 14:00:20 -0500 Subject: [PATCH 454/906] =?UTF-8?q?=F0=9F=94=A7=20Update=20members=20(#106?= =?UTF-8?q?3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/members.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/data/members.yml b/data/members.yml index 062395f32f..4dba986672 100644 --- a/data/members.yml +++ b/data/members.yml @@ -1,3 +1,4 @@ members: - login: tiangolo - login: estebanx64 +- login: alejsdev From a485c4e6a1e05c2a93e73497448b3d9e4545c7f9 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 12 Aug 2024 19:00:38 +0000 Subject: [PATCH 455/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index c44ea093d2..f2d181e884 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* 🔧 Update members. PR [#1063](https://github.com/tiangolo/sqlmodel/pull/1063) by [@tiangolo](https://github.com/tiangolo). * 💄 Add dark-mode logo. PR [#1061](https://github.com/tiangolo/sqlmodel/pull/1061) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update docs.py script to enable dirty reload conditionally. PR [#1060](https://github.com/tiangolo/sqlmodel/pull/1060) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update MkDocs previews. PR [#1058](https://github.com/tiangolo/sqlmodel/pull/1058) by [@tiangolo](https://github.com/tiangolo). From cd1fb5e4aede203a9b6e3e931eb85306d16b3a7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 12 Aug 2024 14:09:56 -0500 Subject: [PATCH 456/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20links=20from=20?= =?UTF-8?q?github.com/tiangolo/sqlmodel=20to=20github.com/fastapi/sqlmodel?= =?UTF-8?q?=20(#1064)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/config.yml | 6 +++--- .github/ISSUE_TEMPLATE/privileged.yml | 2 +- .github/workflows/issue-manager.yml | 2 +- CITATION.cff | 2 +- README.md | 16 ++++++++-------- docs/help.md | 16 ++++++++-------- docs/index.md | 16 ++++++++-------- docs/management-tasks.md | 2 +- mkdocs.yml | 6 +++--- pyproject.toml | 4 ++-- 10 files changed, 36 insertions(+), 36 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 46a5c2c216..b4f46138cd 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -4,10 +4,10 @@ contact_links: about: Please report security vulnerabilities to security@tiangolo.com - name: Question or Problem about: Ask a question or ask about a problem in GitHub Discussions. - url: https://github.com/tiangolo/sqlmodel/discussions/categories/questions + url: https://github.com/fastapi/sqlmodel/discussions/categories/questions - name: Feature Request about: To suggest an idea or ask about a feature, please start with a question saying what you would like to achieve. There might be a way to do it already. - url: https://github.com/tiangolo/sqlmodel/discussions/categories/questions + url: https://github.com/fastapi/sqlmodel/discussions/categories/questions - name: Show and tell about: Show what you built with SQLModel or to be used with SQLModel. - url: https://github.com/tiangolo/sqlmodel/discussions/categories/show-and-tell + url: https://github.com/fastapi/sqlmodel/discussions/categories/show-and-tell diff --git a/.github/ISSUE_TEMPLATE/privileged.yml b/.github/ISSUE_TEMPLATE/privileged.yml index da11e718eb..8cc6002633 100644 --- a/.github/ISSUE_TEMPLATE/privileged.yml +++ b/.github/ISSUE_TEMPLATE/privileged.yml @@ -6,7 +6,7 @@ body: value: | Thanks for your interest in SQLModel! 🚀 - If you are not @tiangolo or he didn't ask you directly to create an issue here, please start the conversation in a [Question in GitHub Discussions](https://github.com/tiangolo/sqlmodel/discussions/categories/questions) instead. + If you are not @tiangolo or he didn't ask you directly to create an issue here, please start the conversation in a [Question in GitHub Discussions](https://github.com/fastapi/sqlmodel/discussions/categories/questions) instead. - type: checkboxes id: privileged attributes: diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml index 4e1a5f7ed2..038491f828 100644 --- a/.github/workflows/issue-manager.yml +++ b/.github/workflows/issue-manager.yml @@ -19,7 +19,7 @@ permissions: jobs: issue-manager: - if: github.repository_owner == 'tiangolo' + if: github.repository_owner == 'fastapi' runs-on: ubuntu-latest steps: - name: Dump GitHub context diff --git a/CITATION.cff b/CITATION.cff index 978031d58e..5ea2d61946 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -12,7 +12,7 @@ authors: family-names: Ramírez email: tiangolo@gmail.com identifiers: -repository-code: 'https://github.com/tiangolo/sqlmodel' +repository-code: 'https://github.com/fastapi/sqlmodel' url: 'https://sqlmodel.tiangolo.com' abstract: >- SQLModel, SQL databases in Python, designed for diff --git a/README.md b/README.md index a9e2241a86..d62606ed73 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,14 @@ SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness.

- - Test + + Test - - Publish + + Publish - - Coverage + + Coverage Package version @@ -23,7 +23,7 @@ **Documentation**: https://sqlmodel.tiangolo.com -**Source Code**: https://github.com/tiangolo/sqlmodel +**Source Code**: https://github.com/fastapi/sqlmodel --- @@ -220,4 +220,4 @@ And at the same time, ✨ it is also a **Pydantic** model ✨. You can use inher ## License -This project is licensed under the terms of the [MIT license](https://github.com/tiangolo/sqlmodel/blob/main/LICENSE). +This project is licensed under the terms of the [MIT license](https://github.com/fastapi/sqlmodel/blob/main/LICENSE). diff --git a/docs/help.md b/docs/help.md index 8dc524b23b..6e5fe581f7 100644 --- a/docs/help.md +++ b/docs/help.md @@ -22,13 +22,13 @@ You can subscribe to the (infrequent) https://github.com/tiangolo/sqlmodel. ⭐️ +You can "star" SQLModel in GitHub (clicking the star button at the top right): https://github.com/fastapi/sqlmodel. ⭐️ By adding a star, other users will be able to find it more easily and see that it has been already useful for others. ## Watch the GitHub repository for releases -You can "watch" SQLModel in GitHub (clicking the "watch" button at the top right): https://github.com/tiangolo/sqlmodel. 👀 +You can "watch" SQLModel in GitHub (clicking the "watch" button at the top right): https://github.com/fastapi/sqlmodel. 👀 There you can select "Releases only". @@ -54,7 +54,7 @@ You can: ## Tweet about **SQLModel** -Tweet about **SQLModel** and let me and others know why you like it. 🎉 +Tweet about **SQLModel** and let me and others know why you like it. 🎉 I love to hear about how **SQLModel** is being used, what you have liked in it, in which project/company are you using it, etc. @@ -62,8 +62,8 @@ I love to hear about how **SQLModel** is being used, what you have liked in it, You can try and help others with their questions in: -* GitHub Discussions -* GitHub Issues +* GitHub Discussions +* GitHub Issues In many cases you might already know the answer for those questions. 🤓 @@ -112,7 +112,7 @@ If they reply, there's a high chance you would have solved their problem, congra ## Watch the GitHub repository -You can "watch" SQLModel in GitHub (clicking the "watch" button at the top right): https://github.com/tiangolo/sqlmodel. 👀 +You can "watch" SQLModel in GitHub (clicking the "watch" button at the top right): https://github.com/fastapi/sqlmodel. 👀 If you select "Watching" instead of "Releases only" you will receive notifications when someone creates a new issue or question. You can also specify that you only want to be notified about new issues, or discussions, or PRs, etc. @@ -120,7 +120,7 @@ Then you can try and help them solve those questions. ## Ask Questions -You can create a new question in the GitHub repository, for example to: +You can create a new question in the GitHub repository, for example to: * Ask a **question** or ask about a **problem**. * Suggest a new **feature**. @@ -214,7 +214,7 @@ Join the 👥 GitHub Discussions, there's a much better chance you will receive help there. +For questions, ask them in GitHub Discussions, there's a much better chance you will receive help there. Use the chat only for other general conversations. diff --git a/docs/index.md b/docs/index.md index 6489f45a7f..f5a34f837b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,14 +12,14 @@ SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness.

- - Test + + Test - - Publish + + Publish - - Coverage + + Coverage Package version @@ -29,7 +29,7 @@ **Documentation**: https://sqlmodel.tiangolo.com -**Source Code**: https://github.com/tiangolo/sqlmodel +**Source Code**: https://github.com/fastapi/sqlmodel --- @@ -233,4 +233,4 @@ And at the same time, ✨ it is also a **Pydantic** model ✨. You can use inher ## License -This project is licensed under the terms of the [MIT license](https://github.com/tiangolo/sqlmodel/blob/main/LICENSE). +This project is licensed under the terms of the [MIT license](https://github.com/fastapi/sqlmodel/blob/main/LICENSE). diff --git a/docs/management-tasks.md b/docs/management-tasks.md index 14a481506b..f8deb992f0 100644 --- a/docs/management-tasks.md +++ b/docs/management-tasks.md @@ -112,4 +112,4 @@ When a question in GitHub Discussions has been answered, mark the answer by clic Many of the current Discussion Questions were migrated from old issues. Many have the label `answered`, that means they were answered when they were issues, but now in GitHub Discussions, it's not known what is the actual response from the messages. -You can filter discussions by `Questions` that are `Unanswered`. +You can filter discussions by `Questions` that are `Unanswered`. diff --git a/mkdocs.yml b/mkdocs.yml index 0ffa87c839..d4e3963ec2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -52,8 +52,8 @@ theme: logo: img/icon-white.svg favicon: img/favicon.png language: en -repo_name: tiangolo/sqlmodel -repo_url: https://github.com/tiangolo/sqlmodel +repo_name: fastapi/sqlmodel +repo_url: https://github.com/fastapi/sqlmodel plugins: # Material for MkDocs search: @@ -201,7 +201,7 @@ extra: Thanks for your feedback! social: - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/sqlmodel + link: https://github.com/fastapi/sqlmodel - icon: fontawesome/brands/twitter link: https://twitter.com/tiangolo - icon: fontawesome/brands/linkedin diff --git a/pyproject.toml b/pyproject.toml index 14a1432ce7..11e83daa15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,9 +40,9 @@ dependencies = [ ] [project.urls] -Homepage = "https://github.com/tiangolo/sqlmodel" +Homepage = "https://github.com/fastapi/sqlmodel" Documentation = "https://sqlmodel.tiangolo.com" -Repository = "https://github.com/tiangolo/sqlmodel" +Repository = "https://github.com/fastapi/sqlmodel" [tool.pdm] version = { source = "file", path = "sqlmodel/__init__.py" } From d55af68fd790a84898327a95de9fb13c10d59d52 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 12 Aug 2024 19:10:18 +0000 Subject: [PATCH 457/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index f2d181e884..233759d5aa 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* 📝 Update links from github.com/tiangolo/sqlmodel to github.com/fastapi/sqlmodel. PR [#1064](https://github.com/fastapi/sqlmodel/pull/1064) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update members. PR [#1063](https://github.com/tiangolo/sqlmodel/pull/1063) by [@tiangolo](https://github.com/tiangolo). * 💄 Add dark-mode logo. PR [#1061](https://github.com/tiangolo/sqlmodel/pull/1061) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update docs.py script to enable dirty reload conditionally. PR [#1060](https://github.com/tiangolo/sqlmodel/pull/1060) by [@tiangolo](https://github.com/tiangolo). From ee44f3b85ec50e0443b8ebf8348f1113ef110b73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 12 Aug 2024 16:38:19 -0500 Subject: [PATCH 458/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20admonitions=20i?= =?UTF-8?q?n=20annotations=20(#1065)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../annotations/en/tutorial002.md | 21 ++++++++---- .../delete/annotations/en/tutorial002.md | 9 +++-- .../annotations/en/test_main_004.md | 7 ++-- .../annotations/en/test_main_004.md | 7 ++-- .../annotations/en/test_main_004.md | 7 ++-- .../select/annotations/en/tutorial002.md | 9 +++-- .../update/annotations/en/tutorial002.md | 9 +++-- .../update/annotations/en/tutorial004.md | 34 +++++++++++++------ 8 files changed, 71 insertions(+), 32 deletions(-) diff --git a/docs_src/tutorial/automatic_id_none_refresh/annotations/en/tutorial002.md b/docs_src/tutorial/automatic_id_none_refresh/annotations/en/tutorial002.md index 725fcb6601..fee38368dc 100644 --- a/docs_src/tutorial/automatic_id_none_refresh/annotations/en/tutorial002.md +++ b/docs_src/tutorial/automatic_id_none_refresh/annotations/en/tutorial002.md @@ -307,8 +307,11 @@ 33. Print the `hero_1`. - !!! info - Even if the `hero_1` wasn't fresh, this would **not** trigger a `refresh` making the **session** use the **engine** to fetch data from the database because it is not accessing an attribute. + /// info + + Even if the `hero_1` wasn't fresh, this would **not** trigger a `refresh` making the **session** use the **engine** to fetch data from the database because it is not accessing an attribute. + + /// Because the `hero_1` is fresh it has all it's data available. @@ -320,8 +323,11 @@ 34. Print the `hero_2`. - !!! info - Even if the `hero_2` wasn't fresh, this would **not** trigger a `refresh` making the **session** use the **engine** to fetch data from the database because it is not accessing an attribute. + /// info + + Even if the `hero_2` wasn't fresh, this would **not** trigger a `refresh` making the **session** use the **engine** to fetch data from the database because it is not accessing an attribute. + + /// Because the `hero_2` is fresh it has all it's data available. @@ -333,8 +339,11 @@ 35. Print the `hero_3`. - !!! info - Even if the `hero_3` wasn't fresh, this would **not** trigger a `refresh` making the **session** use the **engine** to fetch data from the database because it is not accessing an attribute. + /// info + + Even if the `hero_3` wasn't fresh, this would **not** trigger a `refresh` making the **session** use the **engine** to fetch data from the database because it is not accessing an attribute. + + /// Because the `hero_3` is fresh it has all it's data available. diff --git a/docs_src/tutorial/delete/annotations/en/tutorial002.md b/docs_src/tutorial/delete/annotations/en/tutorial002.md index 28dcc50fb3..a6971f67d5 100644 --- a/docs_src/tutorial/delete/annotations/en/tutorial002.md +++ b/docs_src/tutorial/delete/annotations/en/tutorial002.md @@ -14,10 +14,13 @@ 3. Get one hero object, expecting exactly one. - !!! tip - This ensures there's no more than one, and that there's exactly one, not `None`. + /// tip - This would never return `None`, instead it would raise an exception. + This ensures there's no more than one, and that there's exactly one, not `None`. + + This would never return `None`, instead it would raise an exception. + + /// 4. Print the hero object. diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_004.md b/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_004.md index 92cbe77441..de754c5e76 100644 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_004.md +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_004.md @@ -22,5 +22,8 @@ We tell it that with the `poolclass=StaticPool` parameter. - !!! info - You can read more details in the SQLAlchemy documentation about Using a Memory Database in Multiple Threads + /// info + + You can read more details in the SQLAlchemy documentation about Using a Memory Database in Multiple Threads + + /// diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_004.md b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_004.md index 92cbe77441..de754c5e76 100644 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_004.md +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_004.md @@ -22,5 +22,8 @@ We tell it that with the `poolclass=StaticPool` parameter. - !!! info - You can read more details in the SQLAlchemy documentation about Using a Memory Database in Multiple Threads + /// info + + You can read more details in the SQLAlchemy documentation about Using a Memory Database in Multiple Threads + + /// diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_004.md b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_004.md index 92cbe77441..de754c5e76 100644 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_004.md +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/annotations/en/test_main_004.md @@ -22,5 +22,8 @@ We tell it that with the `poolclass=StaticPool` parameter. - !!! info - You can read more details in the SQLAlchemy documentation about Using a Memory Database in Multiple Threads + /// info + + You can read more details in the SQLAlchemy documentation about Using a Memory Database in Multiple Threads + + /// diff --git a/docs_src/tutorial/select/annotations/en/tutorial002.md b/docs_src/tutorial/select/annotations/en/tutorial002.md index 312bd81a94..fa7e6f3a55 100644 --- a/docs_src/tutorial/select/annotations/en/tutorial002.md +++ b/docs_src/tutorial/select/annotations/en/tutorial002.md @@ -16,10 +16,13 @@ 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. + /// tip - But it still uses the same **engine**. We still have one engine for the whole application. + 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. diff --git a/docs_src/tutorial/update/annotations/en/tutorial002.md b/docs_src/tutorial/update/annotations/en/tutorial002.md index eb1a820c0b..7cb7c28ba8 100644 --- a/docs_src/tutorial/update/annotations/en/tutorial002.md +++ b/docs_src/tutorial/update/annotations/en/tutorial002.md @@ -13,10 +13,13 @@ 3. Get one hero object, expecting exactly one. - !!! tip - This ensures there's no more than one, and that there's exactly one, not `None`. + /// tip - This would never return `None`, instead it would raise an exception. + This ensures there's no more than one, and that there's exactly one, not `None`. + + This would never return `None`, instead it would raise an exception. + + /// 4. Print the hero object. diff --git a/docs_src/tutorial/update/annotations/en/tutorial004.md b/docs_src/tutorial/update/annotations/en/tutorial004.md index 8378d1b3a7..bbb713ead5 100644 --- a/docs_src/tutorial/update/annotations/en/tutorial004.md +++ b/docs_src/tutorial/update/annotations/en/tutorial004.md @@ -35,12 +35,15 @@ INFO Engine [no key 0.00020s] ('Captain North America',) ``` - !!! tip - See the `BEGIN` at the top? + /// tip - This is SQLAlchemy automatically starting a transaction for us. + See the `BEGIN` at the top? - This way, we could revert the last changes (if there were some) if we wanted to, even if the SQL to create them was already sent to the database. + This is SQLAlchemy automatically starting a transaction for us. + + This way, we could revert the last changes (if there were some) if we wanted to, even if the SQL to create them was already sent to the database. + + /// 7. Get one hero object for this new query. @@ -98,10 +101,13 @@ INFO Engine COMMIT ``` - !!! tip - See how SQLAlchemy (that powers SQLModel) optimizes the SQL to do as much work as possible in a single batch. + /// tip + + See how SQLAlchemy (that powers SQLModel) optimizes the SQL to do as much work as possible in a single batch. + + Here it updates both heroes in a single SQL query. - Here it updates both heroes in a single SQL query. + /// 16. Refresh the first hero. @@ -115,8 +121,11 @@ INFO Engine [generated in 0.00023s] (2,) ``` - !!! tip - Because we just committed a SQL transaction with `COMMIT`, SQLAlchemy will automatically start a new transaction with `BEGIN`. + /// tip + + Because we just committed a SQL transaction with `COMMIT`, SQLAlchemy will automatically start a new transaction with `BEGIN`. + + /// 17. Refresh the second hero. @@ -129,8 +138,11 @@ INFO Engine [cached since 0.001709s ago] (7,) ``` - !!! tip - SQLAlchemy is still using the previous transaction, so it doesn't have to create a new one. + /// tip + + SQLAlchemy is still using the previous transaction, so it doesn't have to create a new one. + + /// 18. Print the first hero, now updated. From bec06eb80c82b0e819d07a36b9ae06977101149c Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 12 Aug 2024 21:38:39 +0000 Subject: [PATCH 459/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 233759d5aa..8aecd0c85d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* 📝 Update admonitions in annotations. PR [#1065](https://github.com/fastapi/sqlmodel/pull/1065) by [@tiangolo](https://github.com/tiangolo). * 📝 Update links from github.com/tiangolo/sqlmodel to github.com/fastapi/sqlmodel. PR [#1064](https://github.com/fastapi/sqlmodel/pull/1064) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update members. PR [#1063](https://github.com/tiangolo/sqlmodel/pull/1063) by [@tiangolo](https://github.com/tiangolo). * 💄 Add dark-mode logo. PR [#1061](https://github.com/tiangolo/sqlmodel/pull/1061) by [@tiangolo](https://github.com/tiangolo). From 8c8ff7d0ae27ba207cf3e95b4f83141b3ac771df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 12 Aug 2024 18:07:25 -0500 Subject: [PATCH 460/906] =?UTF-8?q?=F0=9F=91=B7=20Add=20GitHub=20Action=20?= =?UTF-8?q?add-to-project=20(#1066)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/add-to-project.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/add-to-project.yml diff --git a/.github/workflows/add-to-project.yml b/.github/workflows/add-to-project.yml new file mode 100644 index 0000000000..701f06fae1 --- /dev/null +++ b/.github/workflows/add-to-project.yml @@ -0,0 +1,22 @@ +name: Add to Project + +on: + pull_request: + types: + - opened + - reopened + - synchronize + issues: + types: + - opened + - reopened + +jobs: + add-to-project: + name: Add to project + runs-on: ubuntu-latest + steps: + - uses: actions/add-to-project@v1.0.2 + with: + project-url: https://github.com/orgs/fastapi/projects/2 + github-token: ${{ secrets.PROJECTS_TOKEN }} From d71b88d8a1f9ca74a09e96fd0cf2fe438eb6723e Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 12 Aug 2024 23:08:02 +0000 Subject: [PATCH 461/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 8aecd0c85d..86121f8768 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* 👷 Add GitHub Action add-to-project. PR [#1066](https://github.com/fastapi/sqlmodel/pull/1066) by [@tiangolo](https://github.com/tiangolo). * 📝 Update admonitions in annotations. PR [#1065](https://github.com/fastapi/sqlmodel/pull/1065) by [@tiangolo](https://github.com/tiangolo). * 📝 Update links from github.com/tiangolo/sqlmodel to github.com/fastapi/sqlmodel. PR [#1064](https://github.com/fastapi/sqlmodel/pull/1064) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update members. PR [#1063](https://github.com/tiangolo/sqlmodel/pull/1063) by [@tiangolo](https://github.com/tiangolo). From 109a54339335bdcd01e481e55262cb730e76a6a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 12 Aug 2024 21:32:46 -0500 Subject: [PATCH 462/906] =?UTF-8?q?=F0=9F=91=B7=20Update=20GitHub=20Action?= =?UTF-8?q?=20add-to-project=20(#1067)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/add-to-project.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/add-to-project.yml b/.github/workflows/add-to-project.yml index 701f06fae1..dccea83f35 100644 --- a/.github/workflows/add-to-project.yml +++ b/.github/workflows/add-to-project.yml @@ -1,11 +1,7 @@ name: Add to Project on: - pull_request: - types: - - opened - - reopened - - synchronize + pull_request_target: issues: types: - opened From af56dc165f0f271a2dbee5d51df681f07a75f291 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 13 Aug 2024 02:33:17 +0000 Subject: [PATCH 463/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 86121f8768..83f8e76f3b 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* 👷 Update GitHub Action add-to-project. PR [#1067](https://github.com/fastapi/sqlmodel/pull/1067) by [@tiangolo](https://github.com/tiangolo). * 👷 Add GitHub Action add-to-project. PR [#1066](https://github.com/fastapi/sqlmodel/pull/1066) by [@tiangolo](https://github.com/tiangolo). * 📝 Update admonitions in annotations. PR [#1065](https://github.com/fastapi/sqlmodel/pull/1065) by [@tiangolo](https://github.com/tiangolo). * 📝 Update links from github.com/tiangolo/sqlmodel to github.com/fastapi/sqlmodel. PR [#1064](https://github.com/fastapi/sqlmodel/pull/1064) by [@tiangolo](https://github.com/tiangolo). From 86f6eeea190f381868eeb2bee5fc91d2f6bf97b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 12 Aug 2024 21:39:05 -0500 Subject: [PATCH 464/906] =?UTF-8?q?=F0=9F=91=B7=20Add=20GitHub=20Action=20?= =?UTF-8?q?labeler=20(#1068)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/labeler.yml | 15 +++++++++++++++ .github/workflows/labeler.yml | 12 ++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 .github/labeler.yml create mode 100644 .github/workflows/labeler.yml diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000000..f4ee6649db --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,15 @@ +docs: + - changed-files: + - any-glob-to-any-file: + - docs/**/* + - docs_src/**/* + +internal: + - changed-files: + - any-glob-to-any-file: + - .github/**/* + - scripts/**/* + - .gitignore + - .pre-commit-config.yaml + - pdm_build.py + - requirements*.txt diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000000..9cbdfda213 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,12 @@ +name: Pull Request Labeler +on: + pull_request_target: + +jobs: + labeler: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v5 From c70992a54ce76c22f617cc45b70974d222552070 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 13 Aug 2024 02:39:31 +0000 Subject: [PATCH 465/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 83f8e76f3b..178c3d509f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* 👷 Add GitHub Action labeler. PR [#1068](https://github.com/fastapi/sqlmodel/pull/1068) by [@tiangolo](https://github.com/tiangolo). * 👷 Update GitHub Action add-to-project. PR [#1067](https://github.com/fastapi/sqlmodel/pull/1067) by [@tiangolo](https://github.com/tiangolo). * 👷 Add GitHub Action add-to-project. PR [#1066](https://github.com/fastapi/sqlmodel/pull/1066) by [@tiangolo](https://github.com/tiangolo). * 📝 Update admonitions in annotations. PR [#1065](https://github.com/fastapi/sqlmodel/pull/1065) by [@tiangolo](https://github.com/tiangolo). From 2f66714e325157adcb0b906ac74a4873625d0295 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 13 Aug 2024 00:22:51 -0500 Subject: [PATCH 466/906] =?UTF-8?q?=F0=9F=91=B7=20Add=20GitHub=20Action=20?= =?UTF-8?q?label-checker=20(#1069)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/labeler.yml | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 9cbdfda213..7cb88936be 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -1,6 +1,13 @@ -name: Pull Request Labeler +name: Pull Request Labeler and Checker on: pull_request_target: + types: + - opened + - synchronize + - reopened + # For label-checker + - labeled + - unlabeled jobs: labeler: @@ -10,3 +17,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/labeler@v5 + # Run this after labeler applied labels + check-labels: + name: Check labels + runs-on: ubuntu-latest + steps: + - uses: docker://agilepathway/pull-request-label-checker:latest + with: + one_of: breaking,security,feature,bug,refactor,upgrade,docs,lang-all,internal + repo_token: ${{ secrets.GITHUB_TOKEN }} From 91a71881b7ff8a339b1a8dd5a3ecc1aca7c7f2f9 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 13 Aug 2024 05:23:09 +0000 Subject: [PATCH 467/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 178c3d509f..8cb9be525e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* 👷 Add GitHub Action label-checker. PR [#1069](https://github.com/fastapi/sqlmodel/pull/1069) by [@tiangolo](https://github.com/tiangolo). * 👷 Add GitHub Action labeler. PR [#1068](https://github.com/fastapi/sqlmodel/pull/1068) by [@tiangolo](https://github.com/tiangolo). * 👷 Update GitHub Action add-to-project. PR [#1067](https://github.com/fastapi/sqlmodel/pull/1067) by [@tiangolo](https://github.com/tiangolo). * 👷 Add GitHub Action add-to-project. PR [#1066](https://github.com/fastapi/sqlmodel/pull/1066) by [@tiangolo](https://github.com/tiangolo). From c9a4bfec4c8ff14801e4a745c5652c6b4c340ed9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 13 Aug 2024 22:53:50 -0500 Subject: [PATCH 468/906] =?UTF-8?q?=F0=9F=91=B7=20Update=20labeler=20GitHu?= =?UTF-8?q?b=20Actions=20permissions=20and=20dependencies=20(#1071)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/labeler.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 7cb88936be..7c5b4616ac 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -19,6 +19,10 @@ jobs: - uses: actions/labeler@v5 # Run this after labeler applied labels check-labels: + needs: + - labeler + permissions: + pull-requests: read name: Check labels runs-on: ubuntu-latest steps: From 714d795e9075fcc68796d33c262d0506969aef65 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 14 Aug 2024 03:56:02 +0000 Subject: [PATCH 469/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 8cb9be525e..1cd3700910 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* 👷 Update labeler GitHub Actions permissions and dependencies. PR [#1071](https://github.com/fastapi/sqlmodel/pull/1071) by [@tiangolo](https://github.com/tiangolo). * 👷 Add GitHub Action label-checker. PR [#1069](https://github.com/fastapi/sqlmodel/pull/1069) by [@tiangolo](https://github.com/tiangolo). * 👷 Add GitHub Action labeler. PR [#1068](https://github.com/fastapi/sqlmodel/pull/1068) by [@tiangolo](https://github.com/tiangolo). * 👷 Update GitHub Action add-to-project. PR [#1067](https://github.com/fastapi/sqlmodel/pull/1067) by [@tiangolo](https://github.com/tiangolo). From 383938155fd07ae399c830c9456b2ff2ca3dee88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 14 Aug 2024 14:02:07 -0500 Subject: [PATCH 470/906] =?UTF-8?q?=F0=9F=91=B7=20Update=20configs=20for?= =?UTF-8?q?=20GitHub=20Action=20labeler,=20to=20add=20only=20one=20label?= =?UTF-8?q?=20(#1072)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/labeler.yml | 33 +++++++++++++++++++++------------ .github/workflows/labeler.yml | 5 +++-- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/.github/labeler.yml b/.github/labeler.yml index f4ee6649db..1af3d5e408 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,15 +1,24 @@ docs: - - changed-files: - - any-glob-to-any-file: - - docs/**/* - - docs_src/**/* + - all: + - changed-files: + - any-glob-to-any-file: + - docs/** + - docs_src/** + - all-globs-to-all-files: + - '!sqlmodel/**' + - '!pyproject.toml' internal: - - changed-files: - - any-glob-to-any-file: - - .github/**/* - - scripts/**/* - - .gitignore - - .pre-commit-config.yaml - - pdm_build.py - - requirements*.txt + - all: + - changed-files: + - any-glob-to-any-file: + - .github/** + - scripts/** + - .gitignore + - .pre-commit-config.yaml + - pdm_build.py + - requirements*.txt + - all-globs-to-all-files: + - '!docs/**' + - '!sqlmodel/**' + - '!pyproject.toml' diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 7c5b4616ac..d62f1668d5 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -1,4 +1,4 @@ -name: Pull Request Labeler and Checker +name: Labels on: pull_request_target: types: @@ -17,13 +17,14 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/labeler@v5 + with: + sync-labels: true # Run this after labeler applied labels check-labels: needs: - labeler permissions: pull-requests: read - name: Check labels runs-on: ubuntu-latest steps: - uses: docker://agilepathway/pull-request-label-checker:latest From 865d4c8417caf5e91f820c8deafe556bcdf86078 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 14 Aug 2024 19:02:27 +0000 Subject: [PATCH 471/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 1cd3700910..2ee51f9670 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* 👷 Update configs for GitHub Action labeler, to add only one label. PR [#1072](https://github.com/fastapi/sqlmodel/pull/1072) by [@tiangolo](https://github.com/tiangolo). * 👷 Update labeler GitHub Actions permissions and dependencies. PR [#1071](https://github.com/fastapi/sqlmodel/pull/1071) by [@tiangolo](https://github.com/tiangolo). * 👷 Add GitHub Action label-checker. PR [#1069](https://github.com/fastapi/sqlmodel/pull/1069) by [@tiangolo](https://github.com/tiangolo). * 👷 Add GitHub Action labeler. PR [#1068](https://github.com/fastapi/sqlmodel/pull/1068) by [@tiangolo](https://github.com/tiangolo). From 756e70a24e41b1a7e65486fc0dd3e5a7ea496540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 15 Aug 2024 13:46:10 -0500 Subject: [PATCH 472/906] =?UTF-8?q?=F0=9F=91=B7=20Do=20not=20sync=20labels?= =?UTF-8?q?=20as=20it=20overrides=20manually=20added=20labels=20(#1073)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/labeler.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index d62f1668d5..c3bb83f9a5 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -17,8 +17,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/labeler@v5 - with: - sync-labels: true # Run this after labeler applied labels check-labels: needs: From 65453903099957eb8a1d792dac240a5303a70242 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 15 Aug 2024 18:46:35 +0000 Subject: [PATCH 473/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 2ee51f9670..218140a214 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* 👷 Do not sync labels as it overrides manually added labels. PR [#1073](https://github.com/fastapi/sqlmodel/pull/1073) by [@tiangolo](https://github.com/tiangolo). * 👷 Update configs for GitHub Action labeler, to add only one label. PR [#1072](https://github.com/fastapi/sqlmodel/pull/1072) by [@tiangolo](https://github.com/tiangolo). * 👷 Update labeler GitHub Actions permissions and dependencies. PR [#1071](https://github.com/fastapi/sqlmodel/pull/1071) by [@tiangolo](https://github.com/tiangolo). * 👷 Add GitHub Action label-checker. PR [#1069](https://github.com/fastapi/sqlmodel/pull/1069) by [@tiangolo](https://github.com/tiangolo). From be94cd77fce86b1f0ddf1e3165a55a1b5fbe8efc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 15 Aug 2024 15:52:00 -0500 Subject: [PATCH 474/906] =?UTF-8?q?=F0=9F=94=A7=20Add=20URLs=20to=20`pypro?= =?UTF-8?q?ject.toml`,=20show=20up=20in=20PyPI=20(#1074)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 11e83daa15..a27309b0af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,8 @@ dependencies = [ Homepage = "https://github.com/fastapi/sqlmodel" Documentation = "https://sqlmodel.tiangolo.com" Repository = "https://github.com/fastapi/sqlmodel" +Issues = "https://github.com/fastapi/sqlmodel/issues" +Changelog = "https://sqlmodel.tiangolo.com/release-notes/" [tool.pdm] version = { source = "file", path = "sqlmodel/__init__.py" } From 805718d78dcc000f5b0bc04b444fcdee127a9170 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 15 Aug 2024 20:53:41 +0000 Subject: [PATCH 475/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 218140a214..1df2bcdee4 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* 🔧 Add URLs to `pyproject.toml`, show up in PyPI. PR [#1074](https://github.com/fastapi/sqlmodel/pull/1074) by [@tiangolo](https://github.com/tiangolo). * 👷 Do not sync labels as it overrides manually added labels. PR [#1073](https://github.com/fastapi/sqlmodel/pull/1073) by [@tiangolo](https://github.com/tiangolo). * 👷 Update configs for GitHub Action labeler, to add only one label. PR [#1072](https://github.com/fastapi/sqlmodel/pull/1072) by [@tiangolo](https://github.com/tiangolo). * 👷 Update labeler GitHub Actions permissions and dependencies. PR [#1071](https://github.com/fastapi/sqlmodel/pull/1071) by [@tiangolo](https://github.com/tiangolo). From e448c339dba0f24d75fe8bf6abc4c02b89acca2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 18 Aug 2024 16:15:32 -0500 Subject: [PATCH 476/906] =?UTF-8?q?=F0=9F=94=A7=20Update=20coverage=20conf?= =?UTF-8?q?ig=20files=20(#1077)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 +- pyproject.toml | 7 +++++++ scripts/test.sh | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8fbbc324b4..86e91cb6ca 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -100,7 +100,7 @@ jobs: - run: ls -la coverage - run: coverage combine coverage - run: coverage report - - run: coverage html --show-contexts --title "Coverage for ${{ github.sha }}" + - run: coverage html --title "Coverage for ${{ github.sha }}" - name: Store coverage HTML uses: actions/upload-artifact@v4 diff --git a/pyproject.toml b/pyproject.toml index a27309b0af..e3b70b5abd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,14 +73,18 @@ optional-dependencies = {} [tool.coverage.run] parallel = true +data_file = "coverage/.coverage" source = [ "docs_src", "tests", "sqlmodel" ] context = '${CONTEXT}' +dynamic_context = "test_function" [tool.coverage.report] +show_missing = true +sort = "-Cover" exclude_lines = [ "pragma: no cover", "@overload", @@ -88,6 +92,9 @@ exclude_lines = [ "if TYPE_CHECKING:", ] +[tool.coverage.html] +show_contexts = true + [tool.mypy] strict = true diff --git a/scripts/test.sh b/scripts/test.sh index 9b758bdbdf..ff4b114b18 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -5,5 +5,5 @@ set -x coverage run -m pytest tests coverage combine -coverage report --show-missing +coverage report coverage html From 45f49d57c1d006565c2fabdb0fa8865cd02c89c7 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 18 Aug 2024 21:16:03 +0000 Subject: [PATCH 477/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 1df2bcdee4..110a3677e0 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* 🔧 Update coverage config files. PR [#1077](https://github.com/fastapi/sqlmodel/pull/1077) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add URLs to `pyproject.toml`, show up in PyPI. PR [#1074](https://github.com/fastapi/sqlmodel/pull/1074) by [@tiangolo](https://github.com/tiangolo). * 👷 Do not sync labels as it overrides manually added labels. PR [#1073](https://github.com/fastapi/sqlmodel/pull/1073) by [@tiangolo](https://github.com/tiangolo). * 👷 Update configs for GitHub Action labeler, to add only one label. PR [#1072](https://github.com/fastapi/sqlmodel/pull/1072) by [@tiangolo](https://github.com/tiangolo). From c96d6b791cbe60b3fcdcc864e91e4e78443ec341 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 23 Aug 2024 14:03:39 -0500 Subject: [PATCH 478/906] =?UTF-8?q?=F0=9F=93=9D=20Add=20docs=20for=20virtu?= =?UTF-8?q?al=20environments=20and=20environment=20variables,=20update=20c?= =?UTF-8?q?ontributing=20(#1082)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- README.md | 2 + docs/contributing.md | 134 ++-- docs/databases.md | 2 +- docs/environment-variables.md | 300 ++++++++ docs/index.md | 2 + docs/install.md | 39 ++ docs/tutorial/fastapi/simple-hero-api.md | 6 +- docs/tutorial/fastapi/tests.md | 4 +- docs/tutorial/index.md | 197 ------ docs/virtual-environments.md | 844 +++++++++++++++++++++++ mkdocs.yml | 3 + 12 files changed, 1284 insertions(+), 251 deletions(-) create mode 100644 docs/environment-variables.md create mode 100644 docs/install.md create mode 100644 docs/virtual-environments.md diff --git a/.gitignore b/.gitignore index 65f9c629b1..9e195bfa79 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ *.pyc -env* .mypy_cache .vscode .idea @@ -12,3 +11,4 @@ coverage.xml site *.db .cache +.venv* diff --git a/README.md b/README.md index d62606ed73..fc38789b7c 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,8 @@ As **SQLModel** is based on **Pydantic** and **SQLAlchemy**, it requires them. T ## Installation +Make sure you create a virtual environment, activate it, and then install SQLModel, for example with: +

```console diff --git a/docs/contributing.md b/docs/contributing.md index 217ed61c56..e3693c5a8d 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -4,49 +4,43 @@ First, you might want to see the basic ways to [help SQLModel and get help](help ## Developing -If you already cloned the repository and you know that you need to deep dive in the code, here are some guidelines to set up your environment. +If you already cloned the sqlmodel repository and you want to deep dive in the code, here are some guidelines to set up your environment. -### Poetry +### Virtual Environment -**SQLModel** uses Poetry to build, package, and publish the project. +Follow the instructions to create and activate a [virtual environment](virtual-environments.md){.internal-link target=_blank} for the internal code of `sqlmodel`. -You can learn how to install it in the Poetry docs. +### Install Requirements Using `pip` -After having Poetry available, you can install the development dependencies: +After activating the environment, install the required packages:
```console -$ poetry install +$ pip install -r requirements.txt ---> 100% ```
-It will also create a virtual environment automatically and will install all the dependencies and your local SQLModel in it. +It will install all the dependencies and your local SQLModel in your local environment. -### Poetry Shell +### Using your Local SQLModel -To use your current environment, and to have access to all the tools in it (for example `pytest` for the tests) enter into a Poetry Shell: +If you create a Python file that imports and uses SQLModel, and run it with the Python from your local environment, it will use your cloned local SQLModel source code. -
- -```console -$ poetry shell -``` +And if you update that local SQLModel source code when you run that Python file again, it will use the fresh version of SQLModel you just edited. -
- -That will set up the environment variables needed and start a new shell with them. +That way, you don't have to "install" your local version to be able to test every change. -#### Using your local SQLModel +/// note | "Technical Details" -If you create a Python file that imports and uses SQLModel, and run it with the Python from your local Poetry environment, it will use your local SQLModel source code. +This only happens when you install using this included `requirements.txt` instead of running `pip install sqlmodel` directly. -And if you update that local SQLModel source code, when you run that Python file again, it will use the fresh version of SQLModel you just edited. +That is because inside the `requirements.txt` file, the local version of SQLModel is marked to be installed in "editable" mode, with the `-e` option. -Poetry takes care of making that work. But of course, it will only work in the current Poetry environment, if you install standard SQLModel in another environment (not from the source in the GitHub repo), that will use the standard SQLModel, not your custom version. +/// ### Format @@ -62,41 +56,36 @@ $ bash scripts/format.sh It will also auto-sort all your imports. -## Docs - -The documentation uses MkDocs with Material for MkDocs. - -All the documentation is in Markdown format in the directory `./docs`. +## Tests -Many of the tutorials have blocks of code. +There is a script that you can run locally to test all the code and generate coverage reports in HTML: -In most of the cases, these blocks of code are actual complete applications that can be run as is. +
-In fact, those blocks of code are not written inside the Markdown, they are Python files in the `./docs_src/` directory. +```console +$ bash scripts/test-cov-html.sh +``` -And those Python files are included/injected in the documentation when generating the site. +
-### Docs for tests +This command generates a directory `./htmlcov/`, if you open the file `./htmlcov/index.html` in your browser, you can explore interactively the regions of code that are covered by the tests, and notice if there is any region missing. -Most of the tests actually run against the example source files in the documentation. +## Docs -This helps making sure that: +First, make sure you set up your environment as described above, that will install all the requirements. -* The documentation is up to date. -* The documentation examples can be run as is. -* Most of the features are covered by the documentation, ensured by test coverage. +### Docs Live During local development, there is a script that builds the site and checks for any changes, live-reloading:
```console -$ bash scripts/docs-live.sh +$ python ./scripts/docs.py live -[INFO] - Building documentation... -[INFO] - Cleaning site directory -[INFO] - Documentation built in 2.74 seconds -[INFO] - Serving on http://127.0.0.1:8008 +[INFO] Serving on http://127.0.0.1:8008 +[INFO] Start watching changes +[INFO] Start detecting changes ```
@@ -105,20 +94,71 @@ It will serve the documentation on `http://127.0.0.1:8008`. That way, you can edit the documentation/source files and see the changes live. -## Tests +/// tip -There is a script that you can run locally to test all the code and generate coverage reports in HTML: +Alternatively, you can perform the same steps that scripts does manually. + +Go into the docs director at `docs/`: + +```console +$ cd docs/ +``` + +Then run `mkdocs` in that directory: + +```console +$ mkdocs serve --dev-addr 8008 +``` + +/// + +#### Typer CLI (Optional) + +The instructions here show you how to use the script at `./scripts/docs.py` with the `python` program directly. + +But you can also use Typer CLI, and you will get autocompletion in your terminal for the commands after installing completion. + +If you install Typer CLI, you can install completion with:
```console -$ bash scripts/test.sh +$ typer --install-completion + +zsh completion installed in /home/user/.bashrc. +Completion will take effect once you restart the terminal. ```
-This command generates a directory `./htmlcov/`, if you open the file `./htmlcov/index.html` in your browser, you can explore interactively the regions of code that are covered by the tests, and notice if there is any region missing. +### Docs Structure + +The documentation uses MkDocs. -## Thanks +And there are extra tools/scripts in place in `./scripts/docs.py`. -Thanks for contributing! ☕ +/// tip + +You don't need to see the code in `./scripts/docs.py`, you just use it in the command line. + +/// + +All the documentation is in Markdown format in the directory `./docs`. + +Many of the tutorials have blocks of code. + +In most of the cases, these blocks of code are actual complete applications that can be run as is. + +In fact, those blocks of code are not written inside the Markdown, they are Python files in the `./docs_src/` directory. + +And those Python files are included/injected in the documentation when generating the site. + +### Docs for Tests + +Most of the tests actually run against the example source files in the documentation. + +This helps to make sure that: + +* The documentation is up-to-date. +* The documentation examples can be run as is. +* Most of the features are covered by the documentation, ensured by test coverage. diff --git a/docs/databases.md b/docs/databases.md index ff5ed9f778..e874767268 100644 --- a/docs/databases.md +++ b/docs/databases.md @@ -4,7 +4,7 @@ Are you a seasoned developer and already know everything about databases? 🤓 -Then you can skip to the [Tutorial - User Guide: First Steps](tutorial/index.md){.internal-link target=_blank} right away. +Then you can skip to the next sections right away. /// diff --git a/docs/environment-variables.md b/docs/environment-variables.md new file mode 100644 index 0000000000..bd8b2a06d0 --- /dev/null +++ b/docs/environment-variables.md @@ -0,0 +1,300 @@ +# Environment Variables + +Before we jump into code, let's cover a bit some of the **basics** that we'll need to understand how to work with Python (and programming) in general. Let's check a bit about **environment variables**. + +/// tip + +If you already know what "environment variables" are and how to use them, feel free to skip this. + +/// + +An environment variable (also known as "**env var**") is a variable that lives **outside** of the Python code, in the **operating system**, and could be read by your Python code (or by other programs as well). + +Environment variables could be useful for handling application **settings**, as part of the **installation** of Python, etc. + +## Create and Use Env Vars + +You can **create** and use environment variables in the **shell (terminal)**, without needing Python: + +//// tab | Linux, macOS, Windows Bash + +
+ +```console +// You could create an env var MY_NAME with +$ export MY_NAME="Wade Wilson" + +// Then you could use it with other programs, like +$ echo "Hello $MY_NAME" + +Hello Wade Wilson +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +// Create an env var MY_NAME +$ $Env:MY_NAME = "Wade Wilson" + +// Use it with other programs, like +$ echo "Hello $Env:MY_NAME" + +Hello Wade Wilson +``` + +
+ +//// + +## Read env vars in Python + +You could also create environment variables **outside** of Python, in the terminal (or with any other method), and then **read them in Python**. + +For example you could have a file `main.py` with: + +```Python hl_lines="3" +import os + +name = os.getenv("MY_NAME", "World") +print(f"Hello {name} from Python") +``` + +/// tip + +The second argument to `os.getenv()` is the default value to return. + +If not provided, it's `None` by default, here we provide `"World"` as the default value to use. + +/// + +Then you could call that Python program: + +//// tab | Linux, macOS, Windows Bash + +
+ +```console +// Here we don't set the env var yet +$ python main.py + +// As we didn't set the env var, we get the default value + +Hello World from Python + +// But if we create an environment variable first +$ export MY_NAME="Wade Wilson" + +// And then call the program again +$ python main.py + +// Now it can read the environment variable + +Hello Wade Wilson from Python +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +// Here we don't set the env var yet +$ python main.py + +// As we didn't set the env var, we get the default value + +Hello World from Python + +// But if we create an environment variable first +$ $Env:MY_NAME = "Wade Wilson" + +// And then call the program again +$ python main.py + +// Now it can read the environment variable + +Hello Wade Wilson from Python +``` + +
+ +//// + +As environment variables can be set outside of the code, but can be read by the code, and don't have to be stored (committed to `git`) with the rest of the files, it's common to use them for configurations or **settings**. + +You can also create an environment variable only for a **specific program invocation**, that is only available to that program, and only for its duration. + +To do that, create it right before the program itself, on the same line: + +
+ +```console +// Create an env var MY_NAME in line for this program call +$ MY_NAME="Wade Wilson" python main.py + +// Now it can read the environment variable + +Hello Wade Wilson from Python + +// The env var no longer exists afterwards +$ python main.py + +Hello World from Python +``` + +
+ +/// tip + +You can read more about it at The Twelve-Factor App: Config. + +/// + +## Types and Validation + +These environment variables can only handle **text strings**, as they are external to Python and have to be compatible with other programs and the rest of the system (and even with different operating systems, as Linux, Windows, macOS). + +That means that **any value** read in Python from an environment variable **will be a `str`**, and any conversion to a different type or any validation has to be done in code. + +## `PATH` Environment Variable + +There is a **special** environment variable called **`PATH`** that is used by the operating systems (Linux, macOS, Windows) to find programs to run. + +The value of the variable `PATH` is a long string that is made of directories separated by a colon `:` on Linux and macOS, and by a semicolon `;` on Windows. + +For example, the `PATH` environment variable could look like this: + +//// tab | Linux, macOS + +```plaintext +/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin +``` + +This means that the system should look for programs in the directories: + +* `/usr/local/bin` +* `/usr/bin` +* `/bin` +* `/usr/sbin` +* `/sbin` + +//// + +//// tab | Windows + +```plaintext +C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32 +``` + +This means that the system should look for programs in the directories: + +* `C:\Program Files\Python312\Scripts` +* `C:\Program Files\Python312` +* `C:\Windows\System32` + +//// + +When you type a **command** in the terminal, the operating system **looks for** the program in **each of those directories** listed in the `PATH` environment variable. + +For example, when you type `python` in the terminal, the operating system looks for a program called `python` in the **first directory** in that list. + +If it finds it, then it will **use it**. Otherwise it keeps looking in the **other directories**. + +### Installing Python and Updating the `PATH` + +When you install Python, you might be asked if you want to update the `PATH` environment variable. + +//// tab | Linux, macOS + +Let's say you install Python and it ends up in a directory `/opt/custompython/bin`. + +If you say yes to update the `PATH` environment variable, then the installer will add `/opt/custompython/bin` to the `PATH` environment variable. + +It could look like this: + +```plaintext +/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/custompython/bin +``` + +This way, when you type `python` in the terminal, the system will find the Python program in `/opt/custompython/bin` (the last directory) and use that one. + +//// + +//// tab | Windows + +Let's say you install Python and it ends up in a directory `C:\opt\custompython\bin`. + +If you say yes to update the `PATH` environment variable, then the installer will add `C:\opt\custompython\bin` to the `PATH` environment variable. + +```plaintext +C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32;C:\opt\custompython\bin +``` + +This way, when you type `python` in the terminal, the system will find the Python program in `C:\opt\custompython\bin` (the last directory) and use that one. + +//// + +This way, when you type `python` in the terminal, the system will find the Python program in `/opt/custompython/bin` (the last directory) and use that one. + +So, if you type: + +
+ +```console +$ python +``` + +
+ +//// tab | Linux, macOS + +The system will **find** the `python` program in `/opt/custompython/bin` and run it. + +It would be roughly equivalent to typing: + +
+ +```console +$ /opt/custompython/bin/python +``` + +
+ +//// + +//// tab | Windows + +The system will **find** the `python` program in `C:\opt\custompython\bin\python` and run it. + +It would be roughly equivalent to typing: + +
+ +```console +$ C:\opt\custompython\bin\python +``` + +
+ +//// + +This information will be useful when learning about [Virtual Environments](virtual-environments.md){.internal-link target=_blank}. + +## Conclusion + +With this you should have a basic understanding of what **environment variables** are and how to use them in Python. + +You can also read more about them in the Wikipedia for Environment Variable. + +In many cases it's not very obvious how environment variables would be useful and applicable right away. But they keep showing up in many different scenarios when you are developing, so it's good to know about them. + +For example, you will need this information in the next section, about [Virtual Environments](virtual-environments.md). diff --git a/docs/index.md b/docs/index.md index f5a34f837b..96062f0363 100644 --- a/docs/index.md +++ b/docs/index.md @@ -78,6 +78,8 @@ As **SQLModel** is based on **Pydantic** and **SQLAlchemy**, it requires them. T ## Installation +Make sure you create a virtual environment, activate it, and then install SQLModel, for example with: +
```console diff --git a/docs/install.md b/docs/install.md new file mode 100644 index 0000000000..8ac2e04c21 --- /dev/null +++ b/docs/install.md @@ -0,0 +1,39 @@ +# Install **SQLModel** + +Create a project directory, create a [virtual environment](virtual-environments.md){.internal-link target=_blank}, activate it, and then install **SQLModel**, for example with: + +
+ +```console +$ pip install sqlmodel +---> 100% +Successfully installed sqlmodel pydantic sqlalchemy +``` + +
+ +As **SQLModel** is built on top of SQLAlchemy and Pydantic, when you install `sqlmodel` they will also be automatically installed. + +## Install DB Browser for SQLite + +Remember that [SQLite is a simple database in a single file](../databases.md#a-single-file-database){.internal-link target=_blank}? + +For most of the tutorial I'll use SQLite for the examples. + +Python has integrated support for SQLite, it is a single file read and processed from Python. And it doesn't need an [External Database Server](../databases.md#a-server-database){.internal-link target=_blank}, so it will be perfect for learning. + +In fact, SQLite is perfectly capable of handling quite big applications. At some point you might want to migrate to a server-based database like PostgreSQL (which is also free). But for now we'll stick to SQLite. + +Through the tutorial I will show you SQL fragments, and Python examples. And I hope (and expect 🧐) you to actually run them, and verify that the database is working as expected and showing you the same data. + +To be able to explore the SQLite file yourself, independent of Python code (and probably at the same time), I recommend you use DB Browser for SQLite. + +It's a great and simple program to interact with SQLite databases (SQLite files) in a nice user interface. + + + +Go ahead and Install DB Browser for SQLite, it's free. + +## Next Steps + +Okay, let's get going! On the next section we'll start the [Tutorial - User Guide](tutorial/index.md). 🚀 diff --git a/docs/tutorial/fastapi/simple-hero-api.md b/docs/tutorial/fastapi/simple-hero-api.md index 0a96118953..fc85f2a008 100644 --- a/docs/tutorial/fastapi/simple-hero-api.md +++ b/docs/tutorial/fastapi/simple-hero-api.md @@ -10,14 +10,14 @@ FastAPI is the framework to create the **web API**. But we also need another type of program to run it, it is called a "**server**". We will use **Uvicorn** for that. And we will install Uvicorn with its *standard* dependencies. -Make sure you [have a virtual environment activated](../index.md#create-a-python-virtual-environment){.internal-link target=_blank}. +Then install FastAPI. -Then install FastAPI and Uvicorn: +Make sure you create a [virtual environment](../../virtual-environments.md){.internal-link target=_blank}, activate it, and then install them, for example with:
```console -$ python -m pip install fastapi "uvicorn[standard]" +$ pip install fastapi "uvicorn[standard]" ---> 100% ``` diff --git a/docs/tutorial/fastapi/tests.md b/docs/tutorial/fastapi/tests.md index 33b17f87d2..922103bdb8 100644 --- a/docs/tutorial/fastapi/tests.md +++ b/docs/tutorial/fastapi/tests.md @@ -42,12 +42,12 @@ If you haven't done testing in FastAPI applications, first check the ```console -$ python -m pip install requests pytest +$ pip install requests pytest ---> 100% ``` diff --git a/docs/tutorial/index.md b/docs/tutorial/index.md index 0c517bbe1a..dd61e8597e 100644 --- a/docs/tutorial/index.md +++ b/docs/tutorial/index.md @@ -2,8 +2,6 @@ In this tutorial you will learn how to use **SQLModel**. -But before we start playing with SQLModel, let's prepare everything else we need. A bit of type annotations, setting up the environment to install everything, and installing DB Browser for SQLite. 🤓 - ## Type hints If you need a refresher about how to use Python type hints (type annotations), check FastAPI's Python types intro. @@ -33,198 +31,3 @@ Using it in your editor is what really shows you the benefits of **SQLModel**, s Running the examples is what will really help you understand what is going on. You can learn a lot more by running some examples and playing around with them than by reading all the docs here. - -## Create a Project - -Please go ahead and create a directory for the project we will work on on this tutorial. - -What I normally do is that I create a directory named `code` inside my home/user directory. - -And inside of that I create one directory per project. - -So, for example: - -
- -```console -// Go to the home directory -$ cd -// Create a directory for all your code projects -$ mkdir code -// Enter into that code directory -$ cd code -// Create a directory for this project -$ mkdir sqlmodel-tutorial -// Enter into that directory -$ cd sqlmodel-tutorial -``` - -
- -/// tip - -Make sure you don't name it also `sqlmodel`, so that you don't end up overriding the name of the package. - -/// - -### Make sure you have Python - -Make sure you have an officially supported version of Python. - -You can check which version you have with: - -
- -```console -$ python3 --version -Python 3.11 -``` - -
- -There's a chance that you have multiple Python versions installed. - -You might want to try with the specific versions, for example with: - -* `python3.12` -* `python3.11` -* `python3.10` -* `python3.9` - -The code would look like this: - -
- -```console -// Let's check with just python3 -$ python3 --version -// This is too old! 😱 -Python 3.5.6 -// Let's see if python3.10 is available -$ python3.10 --version -// Oh, no, this one is not available 😔 -command not found: python3.10 -$ python3.9 --version -// Nice! This works 🎉 -Python 3.9.0 -// In this case, we would continue using python3.9 instead of python3 -``` - -
- -If you have different versions and `python3` is not the latest, make sure you use the latest version you have available. For example `python3.9`. - -If you don't have a valid Python version installed, go and install that first. - -### Create a Python virtual environment - -When writing Python code, you should **always** use virtual environments in one way or another. - -If you don't know what that is, you can read the official tutorial for virtual environments, it's quite simple. - -In very short, a virtual environment is a small directory that contains a copy of Python and all the libraries you need to run your code. - -And when you "activate" it, any package that you install, for example with `pip`, will be installed in that virtual environment. - -/// tip - -There are other tools to manage virtual environments, like Poetry. - -And there are alternatives that are particularly useful for deployment like Docker and other types of containers. In this case, the "virtual environment" is not just the Python standard files and the installed packages, but the whole system. - -/// - -Go ahead and create a Python virtual environment for this project. And make sure to also upgrade `pip`. - -Here are the commands you could use: - -/// tab | Linux, macOS, Linux in Windows - -
- -```console -// Remember that you might need to use python3.9 or similar 💡 -// Create the virtual environment using the module "venv" -$ python3 -m venv env -// ...here it creates the virtual environment in the directory "env" -// Activate the virtual environment -$ source ./env/bin/activate -// Verify that the virtual environment is active -# (env) $$ which python -// The important part is that it is inside the project directory, at "code/sqlmodel-tutorial/env/bin/python" -/home/leela/code/sqlmodel-tutorial/env/bin/python -// Use the module "pip" to install and upgrade the package "pip" 🤯 -# (env) $$ python -m pip install --upgrade pip ----> 100% -Successfully installed pip -``` - -
- -/// - -/// tab | Windows PowerShell - -
- -```console -// Create the virtual environment using the module "venv" -# >$ python3 -m venv env -// ...here it creates the virtual environment in the directory "env" -// Activate the virtual environment -# >$ .\env\Scripts\Activate.ps1 -// Verify that the virtual environment is active -# (env) >$ Get-Command python -// The important part is that it is inside the project directory, at "code\sqlmodel-tutorial\env\python.exe" -CommandType Name Version Source ------------ ---- ------- ------ -Application python 0.0.0.0 C:\Users\leela\code\sqlmodel-tutorial\env\python.exe -// Use the module "pip" to install and upgrade the package "pip" 🤯 -# (env) >$ python3 -m pip install --upgrade pip ----> 100% -Successfully installed pip -``` - -
- -/// - -## Install **SQLModel** - -Now, after making sure we are inside of a virtual environment in some way, we can install **SQLModel**: - -
- -```console -# (env) $$ pip install sqlmodel ----> 100% -Successfully installed sqlmodel pydantic sqlalchemy -``` - -
- -As **SQLModel** is built on top of SQLAlchemy and Pydantic, when you install `sqlmodel` they will also be automatically installed. - -## Install DB Browser for SQLite - -Remember that [SQLite is a simple database in a single file](../databases.md#a-single-file-database){.internal-link target=_blank}? - -For most of the tutorial I'll use SQLite for the examples. - -Python has integrated support for SQLite, it is a single file read and processed from Python. And it doesn't need an [External Database Server](../databases.md#a-server-database){.internal-link target=_blank}, so it will be perfect for learning. - -In fact, SQLite is perfectly capable of handling quite big applications. At some point you might want to migrate to a server-based database like PostgreSQL (which is also free). But for now we'll stick to SQLite. - -Through the tutorial I will show you SQL fragments, and Python examples. And I hope (and expect 🧐) you to actually run them, and verify that the database is working as expected and showing you the same data. - -To be able to explore the SQLite file yourself, independent of Python code (and probably at the same time), I recommend you use DB Browser for SQLite. - -It's a great and simple program to interact with SQLite databases (SQLite files) in a nice user interface. - - - -Go ahead and Install DB Browser for SQLite, it's free. - -## Next Steps - -Okay, let's get going! On the [next section](create-db-and-table-with-db-browser.md) we'll start creating a database. 🚀 diff --git a/docs/virtual-environments.md b/docs/virtual-environments.md new file mode 100644 index 0000000000..541ae52724 --- /dev/null +++ b/docs/virtual-environments.md @@ -0,0 +1,844 @@ +# Virtual Environments + +When you work in Python projects you probably should use a **virtual environment** (or a similar mechanism) to isolate the packages you install for each project. + +/// info + +If you already know about virtual environments, how to create them and use them, you might want to skip this section. 🤓 + +/// + +/// tip + +A **virtual environment** is different than an **environment variable**. + +An **environment variable** is a variable in the system that can be used by programs. + +A **virtual environment** is a directory with some files in it. + +/// + +/// info + +This page will teach you how to use **virtual environments** and how they work. + +If you are ready to adopt a **tool that manages everything** for you (including installing Python), try uv. + +/// + +## Create a Project + +First, create a directory for your project. + +What I normally do is that I create a directory named `code` inside my home/user directory. + +And inside of that I create one directory per project. + +
+ +```console +// Go to the home directory +$ cd +// Create a directory for all your code projects +$ mkdir code +// Enter into that code directory +$ cd code +// Create a directory for this project +$ mkdir awesome-project +// Enter into that project directory +$ cd awesome-project +``` + +
+ +## Create a Virtual Environment + +When you start working on a Python project **for the first time**, create a virtual environment **inside your project**. + +/// tip + +You only need to do this **once per project**, not every time you work. + +/// + +//// tab | `venv` + +To create a virtual environment, you can use the `venv` module that comes with Python. + +
+ +```console +$ python -m venv .venv +``` + +
+ +/// details | What that command means + +* `python`: use the program called `python` +* `-m`: call a module as a script, we'll tell it which module next +* `venv`: use the module called `venv` that normally comes installed with Python +* `.venv`: create the virtual environment in the new directory `.venv` + +/// + +//// + +//// tab | `uv` + +If you have `uv` installed, you can use it to create a virtual environment. + +
+ +```console +$ uv venv +``` + +
+ +/// tip + +By default, `uv` will create a virtual environment in a directory called `.venv`. + +But you could customize it passing an additional argument with the directory name. + +/// + +//// + +That command creates a new virtual environment in a directory called `.venv`. + +/// details | `.venv` or other name + +You could create the virtual environment in a different directory, but there's a convention of calling it `.venv`. + +/// + +## Activate the Virtual Environment + +Activate the new virtual environment so that any Python command you run or package you install uses it. + +/// tip + +Do this **every time** you start a **new terminal session** to work on the project. + +/// + +//// tab | Linux, macOS + +
+ +```console +$ source .venv/bin/activate +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +$ .venv\Scripts\Activate.ps1 +``` + +
+ +//// + +//// tab | Windows Bash + +Or if you use Bash for Windows (e.g. Git Bash): + +
+ +```console +$ source .venv/Scripts/activate +``` + +
+ +//// + +/// tip + +Every time you install a **new package** in that environment, **activate** the environment again. + +This makes sure that if you use a **terminal (CLI) program** installed by that package, you use the one from your virtual environment and not any other that could be installed globally, probably with a different version than what you need. + +/// + +## Check the Virtual Environment is Active + +Check that the virtual environment is active (the previous command worked). + +/// tip + +This is **optional**, but it's a good way to **check** that everything is working as expected and you are using the virtual environment you intended. + +/// + +//// tab | Linux, macOS, Windows Bash + +
+ +```console +$ which python + +/home/user/code/awesome-project/.venv/bin/python +``` + +
+ +If it shows the `python` binary at `.venv/bin/python`, inside of your project (in this case `awesome-project`), then it worked. 🎉 + +//// + +//// tab | Windows PowerShell + +
+ +```console +$ Get-Command python + +C:\Users\user\code\awesome-project\.venv\Scripts\python +``` + +
+ +If it shows the `python` binary at `.venv\Scripts\python`, inside of your project (in this case `awesome-project`), then it worked. 🎉 + +//// + +## Upgrade `pip` + +/// tip + +If you use `uv` you would use it to install things instead of `pip`, so you don't need to upgrade `pip`. 😎 + +/// + +If you are using `pip` to install packages (it comes by default with Python), you should **upgrade** it to the latest version. + +Many exotic errors while installing a package are solved by just upgrading `pip` first. + +/// tip + +You would normally do this **once**, right after you create the virtual environment. + +/// + +Make sure the virtual environment is active (with the command above) and then run: + +
+ +```console +$ python -m pip install --upgrade pip + +---> 100% +``` + +
+ +## Add `.gitignore` + +If you are using **Git** (you should), add a `.gitignore` file to exclude everything in your `.venv` from Git. + +/// tip + +If you used `uv` to create the virtual environment, it already did this for you, you can skip this step. 😎 + +/// + +/// tip + +Do this **once**, right after you create the virtual environment. + +/// + +
+ +```console +$ echo "*" > .venv/.gitignore +``` + +
+ +/// details | What that command means + +* `echo "*"`: will "print" the text `*` in the terminal (the next part changes that a bit) +* `>`: anything printed to the terminal by the command to the left of `>` should not be printed but instead written to the file that goes to the right of `>` +* `.gitignore`: the name of the file where the text should be written + +And `*` for Git means "everything". So, it will ignore everything in the `.venv` directory. + +That command will create a file `.gitignore` with the content: + +```gitignore +* +``` + +/// + +## Install Packages + +After activating the environment, you can install packages in it. + +/// tip + +Do this **once** when installing or upgrading the packages your project needs. + +If you need to upgrade a version or add a new package you would **do this again**. + +/// + +### Install Packages Directly + +If you're in a hurry and don't want to use a file to declare your project's package requirements, you can install them directly. + +/// tip + +It's a (very) good idea to put the packages and versions your program needs in a file (for example `requirements.txt` or `pyproject.toml`). + +/// + +//// tab | `pip` + +
+ +```console +$ pip install sqlmodel + +---> 100% +``` + +
+ +//// + +//// tab | `uv` + +If you have `uv`: + +
+ +```console +$ uv pip install sqlmodel +---> 100% +``` + +
+ +//// + +### Install from `requirements.txt` + +If you have a `requirements.txt`, you can now use it to install its packages. + +//// tab | `pip` + +
+ +```console +$ pip install -r requirements.txt +---> 100% +``` + +
+ +//// + +//// tab | `uv` + +If you have `uv`: + +
+ +```console +$ uv pip install -r requirements.txt +---> 100% +``` + +
+ +//// + +/// details | `requirements.txt` + +A `requirements.txt` with some packages could look like: + +```requirements.txt +sqlmodel==0.13.0 +rich==13.7.1 +``` + +/// + +## Run Your Program + +After you activated the virtual environment, you can run your program, and it will use the Python inside of your virtual environment with the packages you installed there. + +
+ +```console +$ python main.py + +Hello World +``` + +
+ +## Configure Your Editor + +You would probably use an editor, make sure you configure it to use the same virtual environment you created (it will probably autodetect it) so that you can get autocompletion and inline errors. + +For example: + +* VS Code +* PyCharm + +/// tip + +You normally have to do this only **once**, when you create the virtual environment. + +/// + +## Deactivate the Virtual Environment + +Once you are done working on your project you can **deactivate** the virtual environment. + +
+ +```console +$ deactivate +``` + +
+ +This way, when you run `python` it won't try to run it from that virtual environment with the packages installed there. + +## Ready to Work + +Now you're ready to start working on your project. + + + +/// tip + +Do you want to understand what's all that above? + +Continue reading. 👇🤓 + +/// + +## Why Virtual Environments + +To work with SQLModel you need to install Python. + +After that, you would need to **install** SQLModel and any other **packages** you want to use. + +To install packages you would normally use the `pip` command that comes with Python (or similar alternatives). + +Nevertheless, if you just use `pip` directly, the packages would be installed in your **global Python environment** (the global installation of Python). + +### The Problem + +So, what's the problem with installing packages in the global Python environment? + +At some point, you will probably end up writing many different programs that depend on **different packages**. And some of these projects you work on will depend on **different versions** of the same package. 😱 + +For example, you could create a project called `philosophers-stone`, this program depends on another package called **`harry`, using the version `1`**. So, you need to install `harry`. + +```mermaid +flowchart LR + stone(philosophers-stone) -->|requires| harry-1[harry v1] +``` + +Then, at some point later, you create another project called `prisoner-of-azkaban`, and this project also depends on `harry`, but this project needs **`harry` version `3`**. + +```mermaid +flowchart LR + azkaban(prisoner-of-azkaban) --> |requires| harry-3[harry v3] +``` + +But now the problem is, if you install the packages globally (in the global environment) instead of in a local **virtual environment**, you will have to choose which version of `harry` to install. + +If you want to run `philosophers-stone` you will need to first install `harry` version `1`, for example with: + +
+ +```console +$ pip install "harry==1" +``` + +
+ +And then you would end up with `harry` version `1` installed in your global Python environment. + +```mermaid +flowchart LR + subgraph global[global env] + harry-1[harry v1] + end + subgraph stone-project[philosophers-stone project] + stone(philosophers-stone) -->|requires| harry-1 + end +``` + +But then if you want to run `prisoner-of-azkaban`, you will need to uninstall `harry` version `1` and install `harry` version `3` (or just installing version `3` would automatically uninstall version `1`). + +
+ +```console +$ pip install "harry==3" +``` + +
+ +And then you would end up with `harry` version `3` installed in your global Python environment. + +And if you try to run `philosophers-stone` again, there's a chance it would **not work** because it needs `harry` version `1`. + +```mermaid +flowchart LR + subgraph global[global env] + harry-1[harry v1] + style harry-1 fill:#ccc,stroke-dasharray: 5 5 + harry-3[harry v3] + end + subgraph stone-project[philosophers-stone project] + stone(philosophers-stone) -.-x|⛔️| harry-1 + end + subgraph azkaban-project[prisoner-of-azkaban project] + azkaban(prisoner-of-azkaban) --> |requires| harry-3 + end +``` + +/// tip + +It's very common in Python packages to try the best to **avoid breaking changes** in **new versions**, but it's better to be safe, and install newer versions intentionally and when you can run the tests to check everything is working correctly. + +/// + +Now, imagine that with **many** other **packages** that all your **projects depend on**. That's very difficult to manage. And you would probably end up running some projects with some **incompatible versions** of the packages, and not knowing why something isn't working. + +Also, depending on your operating system (e.g. Linux, Windows, macOS), it could have come with Python already installed. And in that case it probably had some packages pre-installed with some specific versions **needed by your system**. If you install packages in the global Python environment, you could end up **breaking** some of the programs that came with your operating system. + +## Where are Packages Installed + +When you install Python, it creates some directories with some files in your computer. + +Some of these directories are the ones in charge of having all the packages you install. + +When you run: + +
+ +```console +// Don't run this now, it's just an example 🤓 +$ pip install sqlmodel +---> 100% +``` + +
+ +That will download a compressed file with the SQLModel code, normally from PyPI. + +It will also **download** files for other packages that SQLModel depends on. + +Then it will **extract** all those files and put them in a directory in your computer. + +By default, it will put those files downloaded and extracted in the directory that comes with your Python installation, that's the **global environment**. + +## What are Virtual Environments + +The solution to the problems of having all the packages in the global environment is to use a **virtual environment for each project** you work on. + +A virtual environment is a **directory**, very similar to the global one, where you can install the packages for a project. + +This way, each project will have it's own virtual environment (`.venv` directory) with its own packages. + +```mermaid +flowchart TB + subgraph stone-project[philosophers-stone project] + stone(philosophers-stone) --->|requires| harry-1 + subgraph venv1[.venv] + harry-1[harry v1] + end + end + subgraph azkaban-project[prisoner-of-azkaban project] + azkaban(prisoner-of-azkaban) --->|requires| harry-3 + subgraph venv2[.venv] + harry-3[harry v3] + end + end + stone-project ~~~ azkaban-project +``` + +## What Does Activating a Virtual Environment Mean + +When you activate a virtual environment, for example with: + +//// tab | Linux, macOS + +
+ +```console +$ source .venv/bin/activate +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +$ .venv\Scripts\Activate.ps1 +``` + +
+ +//// + +//// tab | Windows Bash + +Or if you use Bash for Windows (e.g. Git Bash): + +
+ +```console +$ source .venv/Scripts/activate +``` + +
+ +//// + +That command will create or modify some [environment variables](environment-variables.md){.internal-link target=_blank} that will be available for the next commands. + +One of those variables is the `PATH` variable. + +/// tip + +You can learn more about the `PATH` environment variable in the [Environment Variables](environment-variables.md#path-environment-variable){.internal-link target=_blank} section. + +/// + +Activating a virtual environment adds its path `.venv/bin` (on Linux and macOS) or `.venv\Scripts` (on Windows) to the `PATH` environment variable. + +Let's say that before activating the environment, the `PATH` variable looked like this: + +//// tab | Linux, macOS + +```plaintext +/usr/bin:/bin:/usr/sbin:/sbin +``` + +That means that the system would look for programs in: + +* `/usr/bin` +* `/bin` +* `/usr/sbin` +* `/sbin` + +//// + +//// tab | Windows + +```plaintext +C:\Windows\System32 +``` + +That means that the system would look for programs in: + +* `C:\Windows\System32` + +//// + +After activating the virtual environment, the `PATH` variable would look something like this: + +//// tab | Linux, macOS + +```plaintext +/home/user/code/awesome-project/.venv/bin:/usr/bin:/bin:/usr/sbin:/sbin +``` + +That means that the system will now start looking first look for programs in: + +```plaintext +/home/user/code/awesome-project/.venv/bin +``` + +before looking in the other directories. + +So, when you type `python` in the terminal, the system will find the Python program in + +```plaintext +/home/user/code/awesome-project/.venv/bin/python +``` + +and use that one. + +//// + +//// tab | Windows + +```plaintext +C:\Users\user\code\awesome-project\.venv\Scripts;C:\Windows\System32 +``` + +That means that the system will now start looking first look for programs in: + +```plaintext +C:\Users\user\code\awesome-project\.venv\Scripts +``` + +before looking in the other directories. + +So, when you type `python` in the terminal, the system will find the Python program in + +```plaintext +C:\Users\user\code\awesome-project\.venv\Scripts\python +``` + +and use that one. + +//// + +An important detail is that it will put the virtual environment path at the **beginning** of the `PATH` variable. The system will find it **before** finding any other Python available. This way, when you run `python`, it will use the Python **from the virtual environment** instead of any other `python` (for example, a `python` from a global environment). + +Activating a virtual environment also changes a couple of other things, but this is one of the most important things it does. + +## Checking a Virtual Environment + +When you check if a virtual environment is active, for example with: + +//// tab | Linux, macOS, Windows Bash + +
+ +```console +$ which python + +/home/user/code/awesome-project/.venv/bin/python +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +$ Get-Command python + +C:\Users\user\code\awesome-project\.venv\Scripts\python +``` + +
+ +//// + +That means that the `python` program that will be used is the one **in the virtual environment**. + +you use `which` in Linux and macOS and `Get-Command` in Windows PowerShell. + +The way that command works is that it will go and check in the `PATH` environment variable, going through **each path in order**, looking for the program called `python`. Once it finds it, it will **show you the path** to that program. + +The most important part is that when you call `python`, that is the exact "`python`" that will be executed. + +So, you can confirm if you are in the correct virtual environment. + +/// tip + +It's easy to activate one virtual environment, get one Python, and then **go to another project**. + +And the second project **wouldn't work** because you are using the **incorrect Python**, from a virtual environment for another project. + +It's useful being able to check what `python` is being used. 🤓 + +/// + +## Why Deactivate a Virtual Environment + +For example, you could be working on a project `philosophers-stone`, **activate that virtual environment**, install packages and work with that environment. + +And then you want to work on **another project** `prisoner-of-azkaban`. + +You go to that project: + +
+ +```console +$ cd ~/code/prisoner-of-azkaban +``` + +
+ +If you don't deactivate the virtual environment for `philosophers-stone`, when you run `python` in the terminal, it will try to use the Python from `philosophers-stone`. + +
+ +```console +$ cd ~/code/prisoner-of-azkaban + +$ python main.py + +// Error importing sirius, it's not installed 😱 +Traceback (most recent call last): + File "main.py", line 1, in + import sirius +``` + +
+ +But if you deactivate the virtual environment and activate the new one for `prisoner-of-askaban` then when you run `python` it will use the Python from the virtual environment in `prisoner-of-azkaban`. + +
+ +```console +$ cd ~/code/prisoner-of-azkaban + +// You don't need to be in the old directory to deactivate, you can do it wherever you are, even after going to the other project 😎 +$ deactivate + +// Activate the virtual environment in prisoner-of-azkaban/.venv 🚀 +$ source .venv/bin/activate + +// Now when you run python, it will find the package sirius installed in this virtual environment ✨ +$ python main.py + +I solemnly swear 🐺 +``` + +
+ +## Alternatives + +This is a simple guide to get you started and teach you how everything works **underneath**. + +There are many **alternatives** to managing virtual environments, package dependencies (requirements), projects. + +Once you are ready and want to use a tool to **manage the entire project**, packages dependencies, virtual environments, etc. I would suggest you try uv. + +`uv` can do a lot of things, it can: + +* **Install Python** for you, including different versions +* Manage the **virtual environment** for your projects +* Install **packages** +* Manage package **dependencies and versions** for your project +* Make sure you have an **exact** set of packages and versions to install, including their dependencies, so that you can be sure that you can run your project in production exactly the same as in your computer while developing, this is called **locking** +* And many other things + +## Conclusion + +If you read and understood all this, now **you know much more** about virtual environments than many developers out there. 🤓 + +Knowing these details will most probably be useful in a future time when you are debugging something that seems complex, but you will know **how it all works underneath**. 😎 diff --git a/mkdocs.yml b/mkdocs.yml index d4e3963ec2..f0f4aa17aa 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -71,6 +71,9 @@ nav: - learn/index.md - databases.md - db-to-code.md + - environment-variables.md + - virtual-environments.md + - install.md - Tutorial - User Guide: - tutorial/index.md - tutorial/create-db-and-table-with-db-browser.md From 6c1b011636977890ef04535d9fb80d8afc51501f Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 23 Aug 2024 19:03:56 +0000 Subject: [PATCH 479/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 110a3677e0..187b0b50ba 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Docs +* 📝 Add docs for virtual environments and environment variables, update contributing. PR [#1082](https://github.com/fastapi/sqlmodel/pull/1082) by [@tiangolo](https://github.com/tiangolo). * 📝 Add docs about repo management and team. PR [#1059](https://github.com/tiangolo/sqlmodel/pull/1059) by [@tiangolo](https://github.com/tiangolo). * ✏️ Fix typo in `cascade_delete` docs. PR [#1030](https://github.com/tiangolo/sqlmodel/pull/1030) by [@tiangolo](https://github.com/tiangolo). From 2978d1083481206b8d8045664cf7261fead98765 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 23 Aug 2024 14:35:37 -0500 Subject: [PATCH 480/906] =?UTF-8?q?=F0=9F=91=B7=20Update=20Python=20versio?= =?UTF-8?q?n=20for=20coverage=20(#1083)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 86e91cb6ca..fa691cb27f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -86,7 +86,7 @@ jobs: - uses: actions/setup-python@v5 with: - python-version: '3.8' + python-version: '3.12' - name: Get coverage files uses: actions/download-artifact@v4 From 2d986c1dd844cf52146fe69ff1f2077ec213dbd3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 23 Aug 2024 19:36:13 +0000 Subject: [PATCH 481/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 187b0b50ba..62e7230b7f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,6 +10,7 @@ ### Internal +* 👷 Update Python version for coverage. PR [#1083](https://github.com/fastapi/sqlmodel/pull/1083) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update coverage config files. PR [#1077](https://github.com/fastapi/sqlmodel/pull/1077) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add URLs to `pyproject.toml`, show up in PyPI. PR [#1074](https://github.com/fastapi/sqlmodel/pull/1074) by [@tiangolo](https://github.com/tiangolo). * 👷 Do not sync labels as it overrides manually added labels. PR [#1073](https://github.com/fastapi/sqlmodel/pull/1073) by [@tiangolo](https://github.com/tiangolo). From e946b0d923a484bec4b86cbf25cad2e0537fadcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 23 Aug 2024 23:10:51 -0500 Subject: [PATCH 482/906] =?UTF-8?q?=F0=9F=94=A7=20Update=20lint=20script?= =?UTF-8?q?=20(#1084)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/lint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/lint.sh b/scripts/lint.sh index 4b279b5fcb..7fab52df57 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -5,4 +5,4 @@ set -x mypy sqlmodel ruff check sqlmodel tests docs_src scripts -ruff format sqlmodel tests docs_src --check +ruff format sqlmodel tests docs_src scripts --check From 28f57431d6af5305102822d7432f9aefbeed74cb Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 24 Aug 2024 04:11:30 +0000 Subject: [PATCH 483/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 62e7230b7f..a6ebdd0314 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,6 +10,7 @@ ### Internal +* 🔧 Update lint script. PR [#1084](https://github.com/fastapi/sqlmodel/pull/1084) by [@tiangolo](https://github.com/tiangolo). * 👷 Update Python version for coverage. PR [#1083](https://github.com/fastapi/sqlmodel/pull/1083) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update coverage config files. PR [#1077](https://github.com/fastapi/sqlmodel/pull/1077) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add URLs to `pyproject.toml`, show up in PyPI. PR [#1074](https://github.com/fastapi/sqlmodel/pull/1074) by [@tiangolo](https://github.com/tiangolo). From 160eb908a340e76989fadab08708e72a7213bc5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 23 Aug 2024 23:19:42 -0500 Subject: [PATCH 484/906] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20a=20typo=20i?= =?UTF-8?q?n=20`docs/virtual-environments.md`=20(#1085)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/virtual-environments.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/virtual-environments.md b/docs/virtual-environments.md index 541ae52724..ca48de91d6 100644 --- a/docs/virtual-environments.md +++ b/docs/virtual-environments.md @@ -558,7 +558,7 @@ The solution to the problems of having all the packages in the global environmen A virtual environment is a **directory**, very similar to the global one, where you can install the packages for a project. -This way, each project will have it's own virtual environment (`.venv` directory) with its own packages. +This way, each project will have its own virtual environment (`.venv` directory) with its own packages. ```mermaid flowchart TB From 5afdaeb68f8329aeebcd264bc821039459f2229f Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 24 Aug 2024 04:20:02 +0000 Subject: [PATCH 485/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index a6ebdd0314..1e3f312f2b 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Docs +* ✏️ Fix a typo in `docs/virtual-environments.md`. PR [#1085](https://github.com/fastapi/sqlmodel/pull/1085) by [@tiangolo](https://github.com/tiangolo). * 📝 Add docs for virtual environments and environment variables, update contributing. PR [#1082](https://github.com/fastapi/sqlmodel/pull/1082) by [@tiangolo](https://github.com/tiangolo). * 📝 Add docs about repo management and team. PR [#1059](https://github.com/tiangolo/sqlmodel/pull/1059) by [@tiangolo](https://github.com/tiangolo). * ✏️ Fix typo in `cascade_delete` docs. PR [#1030](https://github.com/tiangolo/sqlmodel/pull/1030) by [@tiangolo](https://github.com/tiangolo). From 57824a27231c02ccc04c096225a4b383a868fa5a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 24 Aug 2024 18:42:31 -0500 Subject: [PATCH 486/906] =?UTF-8?q?=E2=AC=86=20Bump=20ruff=20from=200.4.7?= =?UTF-8?q?=20to=200.6.2=20(#1081)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.7 to 0.6.2. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/v0.4.7...0.6.2) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 089e1a8326..847ce90c4a 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -3,7 +3,7 @@ pytest >=7.0.1,<8.0.0 coverage[toml] >=6.2,<8.0 mypy ==1.4.1 -ruff ==0.4.7 +ruff ==0.6.2 # For FastAPI tests fastapi >=0.103.2 httpx ==0.24.1 From 6e4ec1f0e91176efc864e85e0b29b7f0229a1c7b Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 24 Aug 2024 23:42:56 +0000 Subject: [PATCH 487/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 1e3f312f2b..a377336944 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -11,6 +11,7 @@ ### Internal +* ⬆ Bump ruff from 0.4.7 to 0.6.2. PR [#1081](https://github.com/fastapi/sqlmodel/pull/1081) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔧 Update lint script. PR [#1084](https://github.com/fastapi/sqlmodel/pull/1084) by [@tiangolo](https://github.com/tiangolo). * 👷 Update Python version for coverage. PR [#1083](https://github.com/fastapi/sqlmodel/pull/1083) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update coverage config files. PR [#1077](https://github.com/fastapi/sqlmodel/pull/1077) by [@tiangolo](https://github.com/tiangolo). From 20859a4660fc084c26063bf3021917e1aefb377e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 24 Aug 2024 18:43:08 -0500 Subject: [PATCH 488/906] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#1028)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.5.2 → v0.6.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.2...v0.6.1) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d8828f4421..7532f21b54 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.2 + rev: v0.6.1 hooks: - id: ruff args: From 711693e55e54eee6f577521ea861859029613584 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 24 Aug 2024 23:44:13 +0000 Subject: [PATCH 489/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index a377336944..bb17557cb5 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -11,6 +11,7 @@ ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1028](https://github.com/fastapi/sqlmodel/pull/1028) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump ruff from 0.4.7 to 0.6.2. PR [#1081](https://github.com/fastapi/sqlmodel/pull/1081) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔧 Update lint script. PR [#1084](https://github.com/fastapi/sqlmodel/pull/1084) by [@tiangolo](https://github.com/tiangolo). * 👷 Update Python version for coverage. PR [#1083](https://github.com/fastapi/sqlmodel/pull/1083) by [@tiangolo](https://github.com/tiangolo). From ed6cf3576587a70d4515b8b2ad5fa05bd984d93d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 25 Aug 2024 21:17:30 -0500 Subject: [PATCH 490/906] =?UTF-8?q?=F0=9F=91=B7=20Update=20`latest-changes?= =?UTF-8?q?`=20GitHub=20Action=20(#1087)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/latest-changes.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml index f77dffc074..77a3b3eec6 100644 --- a/.github/workflows/latest-changes.yml +++ b/.github/workflows/latest-changes.yml @@ -30,8 +30,7 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true - - uses: docker://tiangolo/latest-changes:0.2.0 - # - uses: tiangolo/latest-changes@main + - uses: tiangolo/latest-changes@0.3.1 with: token: ${{ secrets.GITHUB_TOKEN }} latest_changes_file: docs/release-notes.md From 9acd934c7feb9596fc1fabf356907a2a42dc8588 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 26 Aug 2024 02:22:45 +0000 Subject: [PATCH 491/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index bb17557cb5..f6f09ad12e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -11,6 +11,7 @@ ### Internal +* 👷 Update `latest-changes` GitHub Action. PR [#1087](https://github.com/fastapi/sqlmodel/pull/1087) by [@tiangolo](https://github.com/tiangolo). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1028](https://github.com/fastapi/sqlmodel/pull/1028) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump ruff from 0.4.7 to 0.6.2. PR [#1081](https://github.com/fastapi/sqlmodel/pull/1081) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔧 Update lint script. PR [#1084](https://github.com/fastapi/sqlmodel/pull/1084) by [@tiangolo](https://github.com/tiangolo). From e4f3ec7a8012759fdc271ea27a20db48d206a531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 31 Aug 2024 11:33:20 +0200 Subject: [PATCH 492/906] =?UTF-8?q?=E2=9C=85=20Refactor=20test=5Fenums=20t?= =?UTF-8?q?o=20make=20them=20independent=20of=20previous=20imports=20(#109?= =?UTF-8?q?5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_enums.py | 49 ++++++++++++-------------------------- tests/test_enums_models.py | 28 ++++++++++++++++++++++ 2 files changed, 43 insertions(+), 34 deletions(-) create mode 100644 tests/test_enums_models.py diff --git a/tests/test_enums.py b/tests/test_enums.py index f0543e90f1..2808f3f9a9 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -1,10 +1,11 @@ -import enum -import uuid +import importlib +import pytest from sqlalchemy import create_mock_engine from sqlalchemy.sql.type_api import TypeEngine -from sqlmodel import Field, SQLModel +from sqlmodel import SQLModel +from . import test_enums_models from .conftest import needs_pydanticv1, needs_pydanticv2 """ @@ -16,30 +17,6 @@ """ -class MyEnum1(str, enum.Enum): - A = "A" - B = "B" - - -class MyEnum2(str, enum.Enum): - C = "C" - D = "D" - - -class BaseModel(SQLModel): - id: uuid.UUID = Field(primary_key=True) - enum_field: MyEnum2 - - -class FlatModel(SQLModel, table=True): - id: uuid.UUID = Field(primary_key=True) - enum_field: MyEnum1 - - -class InheritModel(BaseModel, table=True): - pass - - def pg_dump(sql: TypeEngine, *args, **kwargs): dialect = sql.compile(dialect=postgres_engine.dialect) sql_str = str(dialect).rstrip() @@ -58,7 +35,9 @@ def sqlite_dump(sql: TypeEngine, *args, **kwargs): sqlite_engine = create_mock_engine("sqlite://", sqlite_dump) -def test_postgres_ddl_sql(capsys): +def test_postgres_ddl_sql(clear_sqlmodel, capsys: pytest.CaptureFixture[str]): + assert test_enums_models, "Ensure the models are imported and registered" + importlib.reload(test_enums_models) SQLModel.metadata.create_all(bind=postgres_engine, checkfirst=False) captured = capsys.readouterr() @@ -66,17 +45,19 @@ def test_postgres_ddl_sql(capsys): assert "CREATE TYPE myenum2 AS ENUM ('C', 'D');" in captured.out -def test_sqlite_ddl_sql(capsys): +def test_sqlite_ddl_sql(clear_sqlmodel, capsys: pytest.CaptureFixture[str]): + assert test_enums_models, "Ensure the models are imported and registered" + importlib.reload(test_enums_models) SQLModel.metadata.create_all(bind=sqlite_engine, checkfirst=False) captured = capsys.readouterr() - assert "enum_field VARCHAR(1) NOT NULL" in captured.out + assert "enum_field VARCHAR(1) NOT NULL" in captured.out, captured assert "CREATE TYPE" not in captured.out @needs_pydanticv1 def test_json_schema_flat_model_pydantic_v1(): - assert FlatModel.schema() == { + assert test_enums_models.FlatModel.schema() == { "title": "FlatModel", "type": "object", "properties": { @@ -97,7 +78,7 @@ def test_json_schema_flat_model_pydantic_v1(): @needs_pydanticv1 def test_json_schema_inherit_model_pydantic_v1(): - assert InheritModel.schema() == { + assert test_enums_models.InheritModel.schema() == { "title": "InheritModel", "type": "object", "properties": { @@ -118,7 +99,7 @@ def test_json_schema_inherit_model_pydantic_v1(): @needs_pydanticv2 def test_json_schema_flat_model_pydantic_v2(): - assert FlatModel.model_json_schema() == { + assert test_enums_models.FlatModel.model_json_schema() == { "title": "FlatModel", "type": "object", "properties": { @@ -134,7 +115,7 @@ def test_json_schema_flat_model_pydantic_v2(): @needs_pydanticv2 def test_json_schema_inherit_model_pydantic_v2(): - assert InheritModel.model_json_schema() == { + assert test_enums_models.InheritModel.model_json_schema() == { "title": "InheritModel", "type": "object", "properties": { diff --git a/tests/test_enums_models.py b/tests/test_enums_models.py new file mode 100644 index 0000000000..b46ccb7d2b --- /dev/null +++ b/tests/test_enums_models.py @@ -0,0 +1,28 @@ +import enum +import uuid + +from sqlmodel import Field, SQLModel + + +class MyEnum1(str, enum.Enum): + A = "A" + B = "B" + + +class MyEnum2(str, enum.Enum): + C = "C" + D = "D" + + +class BaseModel(SQLModel): + id: uuid.UUID = Field(primary_key=True) + enum_field: MyEnum2 + + +class FlatModel(SQLModel, table=True): + id: uuid.UUID = Field(primary_key=True) + enum_field: MyEnum1 + + +class InheritModel(BaseModel, table=True): + pass From 4eaf8b9efb561396351feec444eaa2b6a41ea0a5 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 31 Aug 2024 09:33:38 +0000 Subject: [PATCH 493/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index f6f09ad12e..ebf22b8609 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -11,6 +11,7 @@ ### Internal +* ✅ Refactor test_enums to make them independent of previous imports. PR [#1095](https://github.com/fastapi/sqlmodel/pull/1095) by [@tiangolo](https://github.com/tiangolo). * 👷 Update `latest-changes` GitHub Action. PR [#1087](https://github.com/fastapi/sqlmodel/pull/1087) by [@tiangolo](https://github.com/tiangolo). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1028](https://github.com/fastapi/sqlmodel/pull/1028) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump ruff from 0.4.7 to 0.6.2. PR [#1081](https://github.com/fastapi/sqlmodel/pull/1081) by [@dependabot[bot]](https://github.com/apps/dependabot). From a14ab0bd3ca8cd5e6701537654ccf4e60cbc4bea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 31 Aug 2024 11:38:19 +0200 Subject: [PATCH 494/906] =?UTF-8?q?=F0=9F=90=9B=20Fix=20support=20for=20ty?= =?UTF-8?q?pes=20with=20`Optional[Annoated[x,=20f()]]`,=20e.g.=20`id:=20Op?= =?UTF-8?q?tional[pydantic.UUID4]`=20(#1093)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/_compat.py | 24 +++++++++++++++--------- sqlmodel/main.py | 4 ++-- tests/test_annotated_uuid.py | 26 ++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 11 deletions(-) create mode 100644 tests/test_annotated_uuid.py diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index 4018d1bb39..4e80cdc374 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -21,7 +21,7 @@ from pydantic import VERSION as P_VERSION from pydantic import BaseModel from pydantic.fields import FieldInfo -from typing_extensions import get_args, get_origin +from typing_extensions import Annotated, get_args, get_origin # Reassign variable to make it reexported for mypy PYDANTIC_VERSION = P_VERSION @@ -177,16 +177,17 @@ def is_field_noneable(field: "FieldInfo") -> bool: return False return False - def get_type_from_field(field: Any) -> Any: - type_: Any = field.annotation + def get_sa_type_from_type_annotation(annotation: Any) -> Any: # Resolve Optional fields - if type_ is None: + if annotation is None: raise ValueError("Missing field type") - origin = get_origin(type_) + origin = get_origin(annotation) if origin is None: - return type_ + return annotation + elif origin is Annotated: + return get_sa_type_from_type_annotation(get_args(annotation)[0]) if _is_union_type(origin): - bases = get_args(type_) + bases = get_args(annotation) if len(bases) > 2: raise ValueError( "Cannot have a (non-optional) union as a SQLAlchemy field" @@ -197,9 +198,14 @@ def get_type_from_field(field: Any) -> Any: "Cannot have a (non-optional) union as a SQLAlchemy field" ) # Optional unions are allowed - return bases[0] if bases[0] is not NoneType else bases[1] + use_type = bases[0] if bases[0] is not NoneType else bases[1] + return get_sa_type_from_type_annotation(use_type) return origin + def get_sa_type_from_field(field: Any) -> Any: + type_: Any = field.annotation + return get_sa_type_from_type_annotation(type_) + def get_field_metadata(field: Any) -> Any: for meta in field.metadata: if isinstance(meta, (PydanticMetadata, MaxLen)): @@ -444,7 +450,7 @@ def is_field_noneable(field: "FieldInfo") -> bool: ) return field.allow_none # type: ignore[no-any-return, attr-defined] - def get_type_from_field(field: Any) -> Any: + def get_sa_type_from_field(field: Any) -> Any: if isinstance(field.type_, type) and field.shape == SHAPE_SINGLETON: return field.type_ raise ValueError(f"The field {field.name} has no matching SQLAlchemy type") diff --git a/sqlmodel/main.py b/sqlmodel/main.py index d8fced51fa..1597e4e04f 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -71,7 +71,7 @@ get_field_metadata, get_model_fields, get_relationship_to, - get_type_from_field, + get_sa_type_from_field, init_pydantic_private_attrs, is_field_noneable, is_table_model_class, @@ -649,7 +649,7 @@ def get_sqlalchemy_type(field: Any) -> Any: if sa_type is not Undefined: return sa_type - type_ = get_type_from_field(field) + type_ = get_sa_type_from_field(field) metadata = get_field_metadata(field) # Check enums first as an enum can also be a str, needed by Pydantic/FastAPI diff --git a/tests/test_annotated_uuid.py b/tests/test_annotated_uuid.py new file mode 100644 index 0000000000..b0e25ab099 --- /dev/null +++ b/tests/test_annotated_uuid.py @@ -0,0 +1,26 @@ +import uuid +from typing import Optional + +from sqlmodel import Field, Session, SQLModel, create_engine, select + +from tests.conftest import needs_pydanticv2 + + +@needs_pydanticv2 +def test_annotated_optional_types(clear_sqlmodel) -> None: + from pydantic import UUID4 + + class Hero(SQLModel, table=True): + # Pydantic UUID4 is: Annotated[UUID, UuidVersion(4)] + id: Optional[UUID4] = Field(default_factory=uuid.uuid4, primary_key=True) + + engine = create_engine("sqlite:///:memory:") + SQLModel.metadata.create_all(engine) + with Session(engine) as db: + hero = Hero() + db.add(hero) + db.commit() + statement = select(Hero) + result = db.exec(statement).all() + assert len(result) == 1 + assert isinstance(hero.id, uuid.UUID) From feb5ff174764eb32c9ae9d76acb5dfcdbf037ed8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 31 Aug 2024 09:38:41 +0000 Subject: [PATCH 495/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index ebf22b8609..a58974acd2 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Fixes + +* 🐛 Fix support for types with `Optional[Annoated[x, f()]]`, e.g. `id: Optional[pydantic.UUID4]`. PR [#1093](https://github.com/fastapi/sqlmodel/pull/1093) by [@tiangolo](https://github.com/tiangolo). + ### Docs * ✏️ Fix a typo in `docs/virtual-environments.md`. PR [#1085](https://github.com/fastapi/sqlmodel/pull/1085) by [@tiangolo](https://github.com/tiangolo). From 5930fb055101f0b88b218bb4e559321491371ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 31 Aug 2024 11:39:52 +0200 Subject: [PATCH 496/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index a58974acd2..f53583484c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -36,7 +36,7 @@ * 💄 Add dark-mode logo. PR [#1061](https://github.com/tiangolo/sqlmodel/pull/1061) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update docs.py script to enable dirty reload conditionally. PR [#1060](https://github.com/tiangolo/sqlmodel/pull/1060) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update MkDocs previews. PR [#1058](https://github.com/tiangolo/sqlmodel/pull/1058) by [@tiangolo](https://github.com/tiangolo). -* 💄 Update Termynal line-height. PR [#1057](https://github.com/tiangolo/sqlmodel/pull/1057) by [@tiangolo](https://github.com/tiangolo). +* 💄 Update Termynal line-height. PR [#1057](https://github.com/tiangolo/sqlmodel/pull/1057) by [@tiangolo](https://github.com/tiangolo). * 👷 Upgrade build docs configs. PR [#1047](https://github.com/tiangolo/sqlmodel/pull/1047) by [@tiangolo](https://github.com/tiangolo). * 👷 Add alls-green for test-redistribute. PR [#1055](https://github.com/tiangolo/sqlmodel/pull/1055) by [@tiangolo](https://github.com/tiangolo). * 👷 Update docs-previews to handle no docs changes. PR [#1056](https://github.com/tiangolo/sqlmodel/pull/1056) by [@tiangolo](https://github.com/tiangolo). From 016b2baaadc623c97b80cba744198bfecd5b024d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 31 Aug 2024 11:40:12 +0200 Subject: [PATCH 497/906] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.0.?= =?UTF-8?q?22?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 2 ++ sqlmodel/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index f53583484c..631eb3a587 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest Changes +## 0.0.22 + ### Fixes * 🐛 Fix support for types with `Optional[Annoated[x, f()]]`, e.g. `id: Optional[pydantic.UUID4]`. PR [#1093](https://github.com/fastapi/sqlmodel/pull/1093) by [@tiangolo](https://github.com/tiangolo). diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index b02ddc9aa1..f62988f4ac 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.0.21" +__version__ = "0.0.22" # Re-export from SQLAlchemy from sqlalchemy.engine import create_engine as create_engine From 76b9b5d1093ee87f24823d519c6c5a6824dc99fa Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 1 Sep 2024 08:49:40 +0200 Subject: [PATCH 498/906] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#1088)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.6.1 → v0.6.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.1...v0.6.2) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7532f21b54..3175140622 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.1 + rev: v0.6.2 hooks: - id: ruff args: From e2d3153dcca6237384a9a6622d605bd337185279 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 1 Sep 2024 06:50:00 +0000 Subject: [PATCH 499/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 631eb3a587..945f62a439 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Internal + +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1088](https://github.com/fastapi/sqlmodel/pull/1088) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). + ## 0.0.22 ### Fixes From 5c6688e945bbe6947b7046d5c32afde45a644b8d Mon Sep 17 00:00:00 2001 From: Sofie Van Landeghem Date: Mon, 2 Sep 2024 22:13:38 +0200 Subject: [PATCH 500/906] =?UTF-8?q?=F0=9F=92=9A=20Set=20`include-hidden-fi?= =?UTF-8?q?les`=20to=20`True`=20when=20using=20the=20`upload-artifact`=20G?= =?UTF-8?q?H=20action=20(#1098)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit set include-hidden-files to true for actions/upload-artifact@v4 --- .github/workflows/build-docs.yml | 1 + .github/workflows/test.yml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index a239305286..00adbfbc5e 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -77,6 +77,7 @@ jobs: with: name: docs-site path: ./site/** + include-hidden-files: true # https://github.com/marketplace/actions/alls-green#why docs-all-green: # This job does nothing and is only used for the branch protection diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fa691cb27f..fe2b2c025b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -76,6 +76,8 @@ jobs: with: name: coverage-${{ matrix.python-version }}-${{ matrix.pydantic-version }} path: coverage + include-hidden-files: true + coverage-combine: needs: - test @@ -107,6 +109,7 @@ jobs: with: name: coverage-html path: htmlcov + include-hidden-files: true # https://github.com/marketplace/actions/alls-green#why alls-green: # This job does nothing and is only used for the branch protection From 6f6f50cfb1d49b6084c8e570c7b960a001802f1d Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 2 Sep 2024 20:13:56 +0000 Subject: [PATCH 501/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 945f62a439..9a2f6cea72 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Internal +* 💚 Set `include-hidden-files` to `True` when using the `upload-artifact` GH action. PR [#1098](https://github.com/fastapi/sqlmodel/pull/1098) by [@svlandeg](https://github.com/svlandeg). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1088](https://github.com/fastapi/sqlmodel/pull/1088) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). ## 0.0.22 From e46572f2ab335a81aa75ffaee378e9c6e916fb62 Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Tue, 3 Sep 2024 16:13:17 +0200 Subject: [PATCH 502/906] =?UTF-8?q?=F0=9F=93=9D=20Remove=20highlights=20in?= =?UTF-8?q?=20`indexes.md`=20(#1100)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tutorial/indexes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/indexes.md b/docs/tutorial/indexes.md index d0724f5183..fea4289a44 100644 --- a/docs/tutorial/indexes.md +++ b/docs/tutorial/indexes.md @@ -24,7 +24,7 @@ Fine, in that case, you can **sneak peek** the final code to create indexes here //// tab | Python 3.10+ -```Python hl_lines="8 10" +```Python {!./docs_src/tutorial/indexes/tutorial002_py310.py!} ``` @@ -32,7 +32,7 @@ Fine, in that case, you can **sneak peek** the final code to create indexes here //// tab | Python 3.7+ -```Python hl_lines="8 10" +```Python {!./docs_src/tutorial/indexes/tutorial002.py!} ``` From b083aa03f9b2edce047545856adb5ef4755af20a Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 3 Sep 2024 14:13:37 +0000 Subject: [PATCH 503/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 9a2f6cea72..722b5b9b83 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Docs + +* 📝 Remove highlights in `indexes.md` . PR [#1100](https://github.com/fastapi/sqlmodel/pull/1100) by [@alejsdev](https://github.com/alejsdev). + ### Internal * 💚 Set `include-hidden-files` to `True` when using the `upload-artifact` GH action. PR [#1098](https://github.com/fastapi/sqlmodel/pull/1098) by [@svlandeg](https://github.com/svlandeg). From 898e0902d45cf71b43207d37b7f37341b2325a19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 8 Sep 2024 14:01:32 +0200 Subject: [PATCH 504/906] =?UTF-8?q?=F0=9F=91=B7=20Fix=20coverage=20process?= =?UTF-8?q?ing=20in=20CI,=20one=20name=20per=20matrix=20run=20(#1104)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fe2b2c025b..614235e475 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -69,7 +69,7 @@ jobs: - name: Test run: bash scripts/test.sh env: - COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }} + COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}-${{ matrix.pydantic-version }} CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }} - name: Store coverage files uses: actions/upload-artifact@v4 From 2aaa75261e2b9340e2aad5c7962a18d0b5b8e748 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 8 Sep 2024 12:01:53 +0000 Subject: [PATCH 505/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 722b5b9b83..c0cf16c1af 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* 👷 Fix coverage processing in CI, one name per matrix run. PR [#1104](https://github.com/fastapi/sqlmodel/pull/1104) by [@tiangolo](https://github.com/tiangolo). * 💚 Set `include-hidden-files` to `True` when using the `upload-artifact` GH action. PR [#1098](https://github.com/fastapi/sqlmodel/pull/1098) by [@svlandeg](https://github.com/svlandeg). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1088](https://github.com/fastapi/sqlmodel/pull/1088) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From 548a4d06d2a2d4653cf11c52a84f4e00e9da9da9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 8 Sep 2024 14:05:54 +0200 Subject: [PATCH 506/906] =?UTF-8?q?=F0=9F=91=B7=20Update=20`issue-manager.?= =?UTF-8?q?yml`=20(#1103)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/issue-manager.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml index 038491f828..1f5341e358 100644 --- a/.github/workflows/issue-manager.yml +++ b/.github/workflows/issue-manager.yml @@ -2,7 +2,7 @@ name: Issue Manager on: schedule: - - cron: "11 4 * * *" + - cron: "13 18 * * *" issue_comment: types: - created @@ -16,6 +16,7 @@ on: permissions: issues: write + pull-requests: write jobs: issue-manager: @@ -35,8 +36,8 @@ jobs: "delay": 864000, "message": "Assuming the original need was handled, this will be automatically closed now. But feel free to add more comments or create new issues or PRs." }, - "changes-requested": { + "waiting": { "delay": 2628000, - "message": "As this PR had requested changes to be applied but has been inactive for a while, it's now going to be closed. But if there's anyone interested, feel free to create a new PR." + "message": "As this PR has been waiting for the original user for a while but seems to be inactive, it's now going to be closed. But if there's anyone interested, feel free to create a new PR." } } From 9550bb9784a393855361e442a927061829a26fd6 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 8 Sep 2024 12:06:10 +0000 Subject: [PATCH 507/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index c0cf16c1af..ec44016285 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* 👷 Update `issue-manager.yml`. PR [#1103](https://github.com/fastapi/sqlmodel/pull/1103) by [@tiangolo](https://github.com/tiangolo). * 👷 Fix coverage processing in CI, one name per matrix run. PR [#1104](https://github.com/fastapi/sqlmodel/pull/1104) by [@tiangolo](https://github.com/tiangolo). * 💚 Set `include-hidden-files` to `True` when using the `upload-artifact` GH action. PR [#1098](https://github.com/fastapi/sqlmodel/pull/1098) by [@svlandeg](https://github.com/svlandeg). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1088](https://github.com/fastapi/sqlmodel/pull/1088) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From 4173ba34e567470e316d46c4b808ceaa19a71e9c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Sep 2024 13:46:16 +0200 Subject: [PATCH 508/906] =?UTF-8?q?=E2=AC=86=20Bump=20tiangolo/issue-manag?= =?UTF-8?q?er=20from=200.5.0=20to=200.5.1=20(#1107)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [tiangolo/issue-manager](https://github.com/tiangolo/issue-manager) from 0.5.0 to 0.5.1. - [Release notes](https://github.com/tiangolo/issue-manager/releases) - [Commits](https://github.com/tiangolo/issue-manager/compare/0.5.0...0.5.1) --- updated-dependencies: - dependency-name: tiangolo/issue-manager dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/issue-manager.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml index 1f5341e358..b7f5e003e3 100644 --- a/.github/workflows/issue-manager.yml +++ b/.github/workflows/issue-manager.yml @@ -27,7 +27,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: tiangolo/issue-manager@0.5.0 + - uses: tiangolo/issue-manager@0.5.1 with: token: ${{ secrets.GITHUB_TOKEN }} config: > From b5ecb184cf760ea51c5d3c39b2b5978ff1a1c2f3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 18 Sep 2024 11:48:56 +0000 Subject: [PATCH 509/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index ec44016285..32f1d6ddf9 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* ⬆ Bump tiangolo/issue-manager from 0.5.0 to 0.5.1. PR [#1107](https://github.com/fastapi/sqlmodel/pull/1107) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Update `issue-manager.yml`. PR [#1103](https://github.com/fastapi/sqlmodel/pull/1103) by [@tiangolo](https://github.com/tiangolo). * 👷 Fix coverage processing in CI, one name per matrix run. PR [#1104](https://github.com/fastapi/sqlmodel/pull/1104) by [@tiangolo](https://github.com/tiangolo). * 💚 Set `include-hidden-files` to `True` when using the `upload-artifact` GH action. PR [#1098](https://github.com/fastapi/sqlmodel/pull/1098) by [@svlandeg](https://github.com/svlandeg). From fa0ec972ce220a12901e17de882e317455ea7539 Mon Sep 17 00:00:00 2001 From: Solipsistmonkey <103457994+Solipsistmonkey@users.noreply.github.com> Date: Sat, 21 Sep 2024 11:44:22 -0700 Subject: [PATCH 510/906] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo=20in=20?= =?UTF-8?q?documentation=20(#1106)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tutorial/one.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/one.md b/docs/tutorial/one.md index f374d1b4a6..4e770dbc2e 100644 --- a/docs/tutorial/one.md +++ b/docs/tutorial/one.md @@ -710,4 +710,4 @@ Hero: None ## Recap -As querying the SQL database for a single row is a common operation, you know have several tools to do it in a short and simple way. 🎉 +As querying the SQL database for a single row is a common operation, you now have several tools to do it in a short and simple way. 🎉 From e6794c6c7f03283ec120ef9b7a8d270a85e27a8e Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 21 Sep 2024 18:44:43 +0000 Subject: [PATCH 511/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 32f1d6ddf9..9daa2d7bef 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Docs +* ✏️ Fix typo in documentation. PR [#1106](https://github.com/fastapi/sqlmodel/pull/1106) by [@Solipsistmonkey](https://github.com/Solipsistmonkey). * 📝 Remove highlights in `indexes.md` . PR [#1100](https://github.com/fastapi/sqlmodel/pull/1100) by [@alejsdev](https://github.com/alejsdev). ### Internal From 97fa93f587b05b7f5b446fd5b1c4afe5f0285820 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 21 Sep 2024 20:45:13 +0200 Subject: [PATCH 512/906] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#1097)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.6.2 → v0.6.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.2...v0.6.5) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3175140622..4b1b10a68f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.2 + rev: v0.6.5 hooks: - id: ruff args: From 7dbd1a96fc2eb4d6e51011b8ef615eb9b49a0b45 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 21 Sep 2024 18:45:35 +0000 Subject: [PATCH 513/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 9daa2d7bef..315a1e91bb 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1097](https://github.com/fastapi/sqlmodel/pull/1097) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump tiangolo/issue-manager from 0.5.0 to 0.5.1. PR [#1107](https://github.com/fastapi/sqlmodel/pull/1107) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Update `issue-manager.yml`. PR [#1103](https://github.com/fastapi/sqlmodel/pull/1103) by [@tiangolo](https://github.com/tiangolo). * 👷 Fix coverage processing in CI, one name per matrix run. PR [#1104](https://github.com/fastapi/sqlmodel/pull/1104) by [@tiangolo](https://github.com/tiangolo). From 5d682eda33ba5794ec26aae2926402d4a21f9567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 5 Oct 2024 15:04:46 +0200 Subject: [PATCH 514/906] =?UTF-8?q?=F0=9F=91=B7=20Upgrade=20Cloudflare=20G?= =?UTF-8?q?itHub=20Action=20(#1124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-docs.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index ca43310691..36ff27d2c0 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -55,14 +55,14 @@ jobs: # hashFiles returns an empty string if there are no files if: hashFiles('./site/*') id: deploy - uses: cloudflare/pages-action@v1 + env: + PROJECT_NAME: sqlmodel + BRANCH: ${{ ( github.event.workflow_run.head_repository.full_name == github.repository && github.event.workflow_run.head_branch == 'main' && 'main' ) || ( github.event.workflow_run.head_sha ) }} + uses: cloudflare/wrangler-action@v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - projectName: sqlmodel - directory: './site' - gitHubToken: ${{ secrets.GITHUB_TOKEN }} - branch: ${{ ( github.event.workflow_run.head_repository.full_name == github.repository && github.event.workflow_run.head_branch == 'main' && 'main' ) || ( github.event.workflow_run.head_sha ) }} + command: pages deploy ./site --project-name=${{ env.PROJECT_NAME }} --branch=${{ env.BRANCH }} - name: Comment Deploy run: python ./scripts/deploy_docs_status.py env: From 64276fcefbbc994559f921edf0083b4d90165379 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 5 Oct 2024 13:05:04 +0000 Subject: [PATCH 515/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 315a1e91bb..229a00d026 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* 👷 Upgrade Cloudflare GitHub Action. PR [#1124](https://github.com/fastapi/sqlmodel/pull/1124) by [@tiangolo](https://github.com/tiangolo). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1097](https://github.com/fastapi/sqlmodel/pull/1097) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump tiangolo/issue-manager from 0.5.0 to 0.5.1. PR [#1107](https://github.com/fastapi/sqlmodel/pull/1107) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Update `issue-manager.yml`. PR [#1103](https://github.com/fastapi/sqlmodel/pull/1103) by [@tiangolo](https://github.com/tiangolo). From 1eb70dfec953ca96400fbbd45263be3141f19bfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 6 Oct 2024 22:19:59 +0200 Subject: [PATCH 516/906] =?UTF-8?q?=F0=9F=91=B7=20Update=20worfkow=20deplo?= =?UTF-8?q?y-docs-notify=20URL=20(#1126)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 36ff27d2c0..6e099c2069 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -67,7 +67,7 @@ jobs: run: python ./scripts/deploy_docs_status.py env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - DEPLOY_URL: ${{ steps.deploy.outputs.url }} + DEPLOY_URL: ${{ steps.deploy.outputs.deployment-url }} COMMIT_SHA: ${{ github.event.workflow_run.head_sha }} RUN_ID: ${{ github.run_id }} IS_DONE: "true" From 321cd93e619fea8b6e85095b4a13f0ad5efbb7d7 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 6 Oct 2024 20:20:23 +0000 Subject: [PATCH 517/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 229a00d026..b54a29ac64 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* 👷 Update worfkow deploy-docs-notify URL. PR [#1126](https://github.com/fastapi/sqlmodel/pull/1126) by [@tiangolo](https://github.com/tiangolo). * 👷 Upgrade Cloudflare GitHub Action. PR [#1124](https://github.com/fastapi/sqlmodel/pull/1124) by [@tiangolo](https://github.com/tiangolo). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1097](https://github.com/fastapi/sqlmodel/pull/1097) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump tiangolo/issue-manager from 0.5.0 to 0.5.1. PR [#1107](https://github.com/fastapi/sqlmodel/pull/1107) by [@dependabot[bot]](https://github.com/apps/dependabot). From fed464a41cd4777b21186c2c6181a20011adb823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 7 Oct 2024 22:34:07 +0200 Subject: [PATCH 518/906] =?UTF-8?q?=F0=9F=91=B7=20Update=20`labeler.yml`?= =?UTF-8?q?=20(#1128)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/labeler.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index c3bb83f9a5..e8e58015a2 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -17,6 +17,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/labeler@v5 + if: ${{ github.event.action != 'labeled' && github.event.action != 'unlabeled' }} + - run: echo "Done adding labels" # Run this after labeler applied labels check-labels: needs: From ad5b10f4e0c29f2f1d119365d7816accc4b75112 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 7 Oct 2024 20:35:22 +0000 Subject: [PATCH 519/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index b54a29ac64..2c6bcd3aa3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* 👷 Update `labeler.yml`. PR [#1128](https://github.com/fastapi/sqlmodel/pull/1128) by [@tiangolo](https://github.com/tiangolo). * 👷 Update worfkow deploy-docs-notify URL. PR [#1126](https://github.com/fastapi/sqlmodel/pull/1126) by [@tiangolo](https://github.com/tiangolo). * 👷 Upgrade Cloudflare GitHub Action. PR [#1124](https://github.com/fastapi/sqlmodel/pull/1124) by [@tiangolo](https://github.com/tiangolo). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1097](https://github.com/fastapi/sqlmodel/pull/1097) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From 772b1a6fa1561741e889b7126db70acc5dcd17a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 7 Oct 2024 23:00:30 +0200 Subject: [PATCH 520/906] =?UTF-8?q?=F0=9F=94=A8=20Update=20script=20to=20s?= =?UTF-8?q?tandardize=20format=20(#1130)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/format.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/format.sh b/scripts/format.sh index 8414381583..cd79892efe 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -1,4 +1,6 @@ -#!/bin/sh -e +#!/usr/bin/env bash + +set -e set -x ruff check sqlmodel tests docs_src scripts --fix From 368bd664dbb01217321f0f84589e8f5b97594561 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 7 Oct 2024 21:00:57 +0000 Subject: [PATCH 521/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 2c6bcd3aa3..3edac4f975 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* 🔨 Update script to standardize format. PR [#1130](https://github.com/fastapi/sqlmodel/pull/1130) by [@tiangolo](https://github.com/tiangolo). * 👷 Update `labeler.yml`. PR [#1128](https://github.com/fastapi/sqlmodel/pull/1128) by [@tiangolo](https://github.com/tiangolo). * 👷 Update worfkow deploy-docs-notify URL. PR [#1126](https://github.com/fastapi/sqlmodel/pull/1126) by [@tiangolo](https://github.com/tiangolo). * 👷 Upgrade Cloudflare GitHub Action. PR [#1124](https://github.com/fastapi/sqlmodel/pull/1124) by [@tiangolo](https://github.com/tiangolo). From c92fe5018eeda294a87564711d549dcc123853b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 7 Oct 2024 23:05:27 +0200 Subject: [PATCH 522/906] =?UTF-8?q?=E2=9E=95=20Add=20docs=20dependency=20m?= =?UTF-8?q?arkdown-include-variants=20(#1129)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mkdocs.yml | 1 + requirements-docs.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/mkdocs.yml b/mkdocs.yml index f0f4aa17aa..881e5e44fb 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -184,6 +184,7 @@ markdown_extensions: # Other extensions mdx_include: + markdown_include_variants: extra: analytics: diff --git a/requirements-docs.txt b/requirements-docs.txt index 3c00f0a265..73fd0ac155 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -16,3 +16,4 @@ cairosvg==2.7.1 # For griffe, it formats with black typer == 0.12.3 mkdocs-macros-plugin==1.0.5 +markdown-include-variants==0.0.1 From 79ef8d0675887628db428a7d5e1d976fbf9127d8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 7 Oct 2024 21:05:46 +0000 Subject: [PATCH 523/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 3edac4f975..a509ec2edb 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* ➕ Add docs dependency markdown-include-variants. PR [#1129](https://github.com/fastapi/sqlmodel/pull/1129) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update script to standardize format. PR [#1130](https://github.com/fastapi/sqlmodel/pull/1130) by [@tiangolo](https://github.com/tiangolo). * 👷 Update `labeler.yml`. PR [#1128](https://github.com/fastapi/sqlmodel/pull/1128) by [@tiangolo](https://github.com/tiangolo). * 👷 Update worfkow deploy-docs-notify URL. PR [#1126](https://github.com/fastapi/sqlmodel/pull/1126) by [@tiangolo](https://github.com/tiangolo). From aa814e24bc18bd7fcce08c661f228eb41ffd0e96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 7 Oct 2024 23:21:59 +0200 Subject: [PATCH 524/906] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20types=20for=20new?= =?UTF-8?q?=20Pydantic=20(#1131)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/main.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 1597e4e04f..3532e81a8e 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -52,7 +52,7 @@ from sqlalchemy.orm.instrumentation import is_instrumented from sqlalchemy.sql.schema import MetaData from sqlalchemy.sql.sqltypes import LargeBinary, Time, Uuid -from typing_extensions import Literal, deprecated, get_origin +from typing_extensions import Literal, TypeAlias, deprecated, get_origin from ._compat import ( # type: ignore[attr-defined] IS_PYDANTIC_V2, @@ -90,7 +90,12 @@ _T = TypeVar("_T") NoArgAnyCallable = Callable[[], Any] -IncEx = Union[Set[int], Set[str], Dict[int, Any], Dict[str, Any], None] +IncEx: TypeAlias = Union[ + Set[int], + Set[str], + Mapping[int, Union["IncEx", Literal[True]]], + Mapping[str, Union["IncEx", Literal[True]]], +] OnDeleteType = Literal["CASCADE", "SET NULL", "RESTRICT"] @@ -858,8 +863,8 @@ def model_dump( self, *, mode: Union[Literal["json", "python"], str] = "python", - include: IncEx = None, - exclude: IncEx = None, + include: Union[IncEx, None] = None, + exclude: Union[IncEx, None] = None, context: Union[Dict[str, Any], None] = None, by_alias: bool = False, exclude_unset: bool = False, @@ -908,8 +913,8 @@ def model_dump( def dict( self, *, - include: IncEx = None, - exclude: IncEx = None, + include: Union[IncEx, None] = None, + exclude: Union[IncEx, None] = None, by_alias: bool = False, exclude_unset: bool = False, exclude_defaults: bool = False, From abbc92bc202453f572d100e2723fee18851c6bff Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 7 Oct 2024 21:22:16 +0000 Subject: [PATCH 525/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index a509ec2edb..8a4218cfba 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Refactors + +* 🚨 Fix types for new Pydantic. PR [#1131](https://github.com/fastapi/sqlmodel/pull/1131) by [@tiangolo](https://github.com/tiangolo). + ### Docs * ✏️ Fix typo in documentation. PR [#1106](https://github.com/fastapi/sqlmodel/pull/1106) by [@Solipsistmonkey](https://github.com/Solipsistmonkey). From f1d5262ca60035771d51e31c079f1bad70ffdbf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 12 Oct 2024 15:08:11 +0200 Subject: [PATCH 526/906] =?UTF-8?q?=F0=9F=91=B7=20Use=20uv=20in=20CI=20(#1?= =?UTF-8?q?135)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 23 +++++++++++------ .github/workflows/deploy-docs.yml | 17 ++++++++----- .github/workflows/smokeshow.yml | 16 +++++++++--- .github/workflows/test.yml | 41 ++++++++++++++++++------------- requirements-github-actions.txt | 1 + 5 files changed, 63 insertions(+), 35 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 00adbfbc5e..c35a212039 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -7,6 +7,11 @@ on: types: - opened - synchronize + +env: + UV_SYSTEM_PYTHON: 1 + + jobs: changes: runs-on: ubuntu-latest @@ -52,17 +57,19 @@ jobs: uses: actions/setup-python@v5 with: python-version: "3.11" - - uses: actions/cache@v4 - id: cache + - name: Setup uv + uses: astral-sh/setup-uv@v3 with: - path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-docs.txt', 'requirements-docs-insiders.txt', 'requirements-docs-tests.txt') }}-v02 + version: "0.4.15" + enable-cache: true + cache-dependency-glob: | + requirements**.txt + pyproject.toml - name: Install docs extras - if: steps.cache.outputs.cache-hit != 'true' - run: pip install -r requirements-docs.txt + run: uv pip install -r requirements-docs.txt - name: Install Material for MkDocs Insiders - if: ( github.event_name != 'pull_request' || github.secret_source == 'Actions' ) && steps.cache.outputs.cache-hit != 'true' - run: pip install -r requirements-docs-insiders.txt + if: ( github.event_name != 'pull_request' || github.secret_source == 'Actions' ) + run: uv pip install -r requirements-docs-insiders.txt env: TOKEN: ${{ secrets.SQLMODEL_MKDOCS_MATERIAL_INSIDERS }} - uses: actions/cache@v4 diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 6e099c2069..6849bd782f 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -12,6 +12,9 @@ permissions: pull-requests: write statuses: write +env: + UV_SYSTEM_PYTHON: 1 + jobs: deploy-docs: runs-on: ubuntu-latest @@ -25,14 +28,16 @@ jobs: uses: actions/setup-python@v5 with: python-version: "3.11" - - uses: actions/cache@v4 - id: cache + - name: Setup uv + uses: astral-sh/setup-uv@v3 with: - path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-github-actions-${{ env.pythonLocation }}-${{ hashFiles('requirements-github-actions.txt') }}-v01 + version: "0.4.15" + enable-cache: true + cache-dependency-glob: | + requirements**.txt + pyproject.toml - name: Install GitHub Actions dependencies - if: steps.cache.outputs.cache-hit != 'true' - run: pip install -r requirements-github-actions.txt + run: uv pip install -r requirements-github-actions.txt - name: Deploy Docs Status Pending run: python ./scripts/deploy_docs_status.py env: diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index bc37a92e78..2280a9de62 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -8,6 +8,9 @@ on: permissions: statuses: write +env: + UV_SYSTEM_PYTHON: 1 + jobs: smokeshow: if: ${{ github.event.workflow_run.conclusion == 'success' }} @@ -17,16 +20,21 @@ jobs: - uses: actions/setup-python@v5 with: python-version: '3.9' - - - run: pip install smokeshow - + - name: Setup uv + uses: astral-sh/setup-uv@v3 + with: + version: "0.4.15" + enable-cache: true + cache-dependency-glob: | + requirements**.txt + pyproject.toml + - run: uv pip install -r requirements-github-actions.txt - uses: actions/download-artifact@v4 with: name: coverage-html path: htmlcov github-token: ${{ secrets.GITHUB_TOKEN }} run-id: ${{ github.event.workflow_run.id }} - - run: smokeshow upload htmlcov env: SMOKESHOW_GITHUB_STATUS_DESCRIPTION: Coverage {coverage-percentage} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 614235e475..3509d5d3df 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,6 +18,9 @@ on: # cron every week on monday - cron: "0 0 * * 1" +env: + UV_SYSTEM_PYTHON: 1 + jobs: test: runs-on: ubuntu-latest @@ -34,33 +37,34 @@ jobs: - pydantic-v1 - pydantic-v2 fail-fast: false - steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + - name: Setup uv + uses: astral-sh/setup-uv@v3 + with: + version: "0.4.15" + enable-cache: true + cache-dependency-glob: | + requirements**.txt + pyproject.toml # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true - - uses: actions/cache@v4 - id: cache - with: - path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-v01 - name: Install Dependencies - if: steps.cache.outputs.cache-hit != 'true' - run: pip install -r requirements-tests.txt + run: uv pip install -r requirements-tests.txt - name: Install Pydantic v1 if: matrix.pydantic-version == 'pydantic-v1' - run: pip install --upgrade "pydantic>=1.10.0,<2.0.0" + run: uv pip install --upgrade "pydantic>=1.10.0,<2.0.0" - name: Install Pydantic v2 if: matrix.pydantic-version == 'pydantic-v2' - run: pip install --upgrade "pydantic>=2.0.2,<3.0.0" "typing-extensions==4.6.1" + run: uv pip install --upgrade "pydantic>=2.0.2,<3.0.0" "typing-extensions==4.6.1" - name: Lint # Do not run on Python 3.7 as mypy behaves differently if: matrix.python-version != '3.7' && matrix.pydantic-version == 'pydantic-v2' @@ -82,28 +86,31 @@ jobs: needs: - test runs-on: ubuntu-latest - steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 with: python-version: '3.12' - + - name: Setup uv + uses: astral-sh/setup-uv@v3 + with: + version: "0.4.15" + enable-cache: true + cache-dependency-glob: | + requirements**.txt + pyproject.toml - name: Get coverage files uses: actions/download-artifact@v4 with: pattern: coverage-* path: coverage merge-multiple: true - - - run: pip install coverage[toml] - + - name: Install Dependencies + run: uv pip install -r requirements-tests.txt - run: ls -la coverage - run: coverage combine coverage - run: coverage report - run: coverage html --title "Coverage for ${{ github.sha }}" - - name: Store coverage HTML uses: actions/upload-artifact@v4 with: diff --git a/requirements-github-actions.txt b/requirements-github-actions.txt index 559dc06fb2..a6dace544f 100644 --- a/requirements-github-actions.txt +++ b/requirements-github-actions.txt @@ -2,3 +2,4 @@ PyGithub>=2.3.0,<3.0.0 pydantic>=2.5.3,<3.0.0 pydantic-settings>=2.1.0,<3.0.0 httpx>=0.27.0,<0.28.0 +smokeshow From 74c53203fc26d4f05d3c7909c19dd084a3f03aca Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 12 Oct 2024 13:08:37 +0000 Subject: [PATCH 527/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 8a4218cfba..d0078b20d8 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -13,6 +13,7 @@ ### Internal +* 👷 Use uv in CI. PR [#1135](https://github.com/fastapi/sqlmodel/pull/1135) by [@tiangolo](https://github.com/tiangolo). * ➕ Add docs dependency markdown-include-variants. PR [#1129](https://github.com/fastapi/sqlmodel/pull/1129) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update script to standardize format. PR [#1130](https://github.com/fastapi/sqlmodel/pull/1130) by [@tiangolo](https://github.com/tiangolo). * 👷 Update `labeler.yml`. PR [#1128](https://github.com/fastapi/sqlmodel/pull/1128) by [@tiangolo](https://github.com/tiangolo). From 313783e294fc390850696df98e9343bb71bc5501 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 12 Oct 2024 15:54:21 +0200 Subject: [PATCH 528/906] =?UTF-8?q?=F0=9F=91=B7=20Fix=20smokeshow,=20check?= =?UTF-8?q?out=20files=20on=20CI=20(#1136)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/smokeshow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index 2280a9de62..651f838da8 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -15,8 +15,8 @@ jobs: smokeshow: if: ${{ github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-latest - steps: + - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.9' From 4aef35e81ddd8f711b1a1953323d50ecf21c75ba Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 12 Oct 2024 13:55:59 +0000 Subject: [PATCH 529/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index d0078b20d8..02a1d247dd 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -13,6 +13,7 @@ ### Internal +* 👷 Fix smokeshow, checkout files on CI. PR [#1136](https://github.com/fastapi/sqlmodel/pull/1136) by [@tiangolo](https://github.com/tiangolo). * 👷 Use uv in CI. PR [#1135](https://github.com/fastapi/sqlmodel/pull/1135) by [@tiangolo](https://github.com/tiangolo). * ➕ Add docs dependency markdown-include-variants. PR [#1129](https://github.com/fastapi/sqlmodel/pull/1129) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update script to standardize format. PR [#1130](https://github.com/fastapi/sqlmodel/pull/1130) by [@tiangolo](https://github.com/tiangolo). From 87257b08bffa3b7d98ae1080232f4bb839d3aea6 Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Tue, 15 Oct 2024 12:39:19 +0200 Subject: [PATCH 530/906] =?UTF-8?q?=F0=9F=91=B7=20Update=20issue=20manager?= =?UTF-8?q?=20workflow=20(#1137)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- .github/workflows/issue-manager.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml index b7f5e003e3..0737b40a8f 100644 --- a/.github/workflows/issue-manager.yml +++ b/.github/workflows/issue-manager.yml @@ -39,5 +39,9 @@ jobs: "waiting": { "delay": 2628000, "message": "As this PR has been waiting for the original user for a while but seems to be inactive, it's now going to be closed. But if there's anyone interested, feel free to create a new PR." + }, + "invalid": { + "delay": 0, + "message": "This was marked as invalid and will be closed now. If this is an error, please provide additional details." } } From 059970f0246e3d850c8ae06867037c336d9a94d8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 15 Oct 2024 10:39:42 +0000 Subject: [PATCH 531/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 02a1d247dd..a55371f002 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -13,6 +13,7 @@ ### Internal +* 👷 Update issue manager workflow. PR [#1137](https://github.com/fastapi/sqlmodel/pull/1137) by [@alejsdev](https://github.com/alejsdev). * 👷 Fix smokeshow, checkout files on CI. PR [#1136](https://github.com/fastapi/sqlmodel/pull/1136) by [@tiangolo](https://github.com/tiangolo). * 👷 Use uv in CI. PR [#1135](https://github.com/fastapi/sqlmodel/pull/1135) by [@tiangolo](https://github.com/tiangolo). * ➕ Add docs dependency markdown-include-variants. PR [#1129](https://github.com/fastapi/sqlmodel/pull/1129) by [@tiangolo](https://github.com/tiangolo). From 1999c7748169005672afb7d0c6aea694b0edd925 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 26 Oct 2024 19:29:02 +0200 Subject: [PATCH 532/906] =?UTF-8?q?=F0=9F=93=9D=20Fix=20internal=20links?= =?UTF-8?q?=20in=20docs=20(#1148)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/databases.md | 10 +++++----- docs/db-to-code.md | 5 ++--- docs/install.md | 4 ++-- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/docs/databases.md b/docs/databases.md index e874767268..d65a7431f4 100644 --- a/docs/databases.md +++ b/docs/databases.md @@ -68,7 +68,7 @@ There are many databases of many types. A database could be a single file called `heroes.db`, managed with code in a very efficient way. An example would be SQLite, more about that on a bit. -![database as a single file](/img/databases/single-file.svg) +![database as a single file](img/databases/single-file.svg) ### A server database @@ -80,11 +80,11 @@ In this case, your code would talk to this server application instead of reading The database could be located in a different server/machine: -![database in an external server](/img/databases/external-server.svg) +![database in an external server](img/databases/external-server.svg) Or the database could be located in the same server/machine: -![database in the same server](/img/databases/same-server.svg) +![database in the same server](img/databases/same-server.svg) The most important aspect of these types of databases is that **your code doesn't read or modify** the files containing the data directly. @@ -98,7 +98,7 @@ In some cases, the database could even be a group of server applications running In this case, your code would talk to one or more of these server applications running on different machines. -![distributed database in multiple servers](/img/databases/multiple-servers.svg) +![distributed database in multiple servers](img/databases/multiple-servers.svg) Most of the databases that work as server applications also support multiple servers in one way or another. @@ -257,7 +257,7 @@ For example, the table for the teams has the ID `1` for the team `Preventers` an As these **primary key** IDs can uniquely identify each row on the table for teams, we can now go to the table for heroes and refer to those IDs in the table for teams. -table relationships +![table relationships](img/databases/relationships.svg) So, in the table for heroes, we use the `team_id` column to define a relationship to the *foreign* table for teams. Each value in the `team_id` column on the table with heroes will be the same value as the `id` column of one row in the table with teams. diff --git a/docs/db-to-code.md b/docs/db-to-code.md index 537c583d3f..3d289d75fa 100644 --- a/docs/db-to-code.md +++ b/docs/db-to-code.md @@ -236,8 +236,7 @@ database.execute( ).all() ``` - - +![](img/db-to-code/autocompletion01.png){class="shadow"} ## ORMs and SQL @@ -280,7 +279,7 @@ For example this **Relation** or table: * **Mapper**: this comes from Math, when there's something that can convert from some set of things to another, that's called a "**mapping function**". That's where the **Mapper** comes from. -![Squares to Triangles Mapper](/img/db-to-code/mapper.svg) +![Squares to Triangles Mapper](img/db-to-code/mapper.svg) We could also write a **mapping function** in Python that converts from the *set of lowercase letters* to the *set of uppercase letters*, like this: diff --git a/docs/install.md b/docs/install.md index 8ac2e04c21..129df3512b 100644 --- a/docs/install.md +++ b/docs/install.md @@ -16,11 +16,11 @@ As **SQLModel** is built on top of (which is also free). But for now we'll stick to SQLite. From 1406429fdf9f05376ad9a5f8123bf43f581e3fdb Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 26 Oct 2024 17:29:19 +0000 Subject: [PATCH 533/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index a55371f002..ee71cfe1af 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Docs +* 📝 Fix internal links in docs. PR [#1148](https://github.com/fastapi/sqlmodel/pull/1148) by [@tiangolo](https://github.com/tiangolo). * ✏️ Fix typo in documentation. PR [#1106](https://github.com/fastapi/sqlmodel/pull/1106) by [@Solipsistmonkey](https://github.com/Solipsistmonkey). * 📝 Remove highlights in `indexes.md` . PR [#1100](https://github.com/fastapi/sqlmodel/pull/1100) by [@alejsdev](https://github.com/alejsdev). From 45bc96e8a336c33bb69c93a5445ffdc9611dfe07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 27 Oct 2024 00:05:08 +0200 Subject: [PATCH 534/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20includes=20for?= =?UTF-8?q?=20`docs/tutorial/create-db-and-table.md`=20(#1149)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tutorial/create-db-and-table.md | 372 +-------------------------- 1 file changed, 11 insertions(+), 361 deletions(-) diff --git a/docs/tutorial/create-db-and-table.md b/docs/tutorial/create-db-and-table.md index de820ab760..978413c334 100644 --- a/docs/tutorial/create-db-and-table.md +++ b/docs/tutorial/create-db-and-table.md @@ -41,45 +41,7 @@ That's why this package is called `SQLModel`. Because it's mainly used to create For that, we will import `SQLModel` (plus other things we will also use) and create a class `Hero` that inherits from `SQLModel` and represents the **table model** for our heroes: -//// tab | Python 3.10+ - -```Python hl_lines="1 4" -{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py[ln:1-8]!} - -# More code here later 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="3 6" -{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-10]!} - -# More code here later 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/create_db_and_table/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py ln[1:8] hl[1,4] *} This class `Hero` **represents the table** for our heroes. And each instance we create later will **represent a row** in the table. @@ -101,45 +63,7 @@ The name of each of these variables will be the name of the column in the table. And the type of each of them will also be the type of table column: -//// tab | Python 3.10+ - -```Python hl_lines="1 5-8" -{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py[ln:1-8]!} - -# More code here later 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="1 3 7-10" -{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-10]!} - -# More code here later 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/create_db_and_table/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py ln[1:8] hl[1,5:8] *} Let's now see with more detail these field/column declarations. @@ -153,45 +77,7 @@ That is the standard way to declare that something "could be an `int` or `None`" And we also set the default value of `age` to `None`. -//// tab | Python 3.10+ - -```Python hl_lines="8" -{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py[ln:1-8]!} - -# More code here later 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="1 10" -{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-10]!} - -# More code here later 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/create_db_and_table/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py ln[1:8] hl[8] *} /// tip @@ -221,45 +107,7 @@ So, we need to mark `id` as the **primary key**. To do that, we use the special `Field` function from `sqlmodel` and set the argument `primary_key=True`: -//// tab | Python 3.10+ - -```Python hl_lines="1 5" -{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py[ln:1-8]!} - -# More code here later 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="3 7" -{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-10]!} - -# More code here later 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/create_db_and_table/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py ln[1:8] hl[1,5] *} That way, we tell **SQLModel** that this `id` field/column is the primary key of the table. @@ -302,45 +150,7 @@ If you have a server database (for example PostgreSQL or MySQL), the **engine** Creating the **engine** is very simple, just call `create_engine()` with a URL for the database to use: -//// tab | Python 3.10+ - -```Python hl_lines="1 14" -{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py[ln:1-16]!} - -# More code here later 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="3 16" -{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-18]!} - -# More code here later 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/create_db_and_table/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py ln[1:16] hl[1,14] *} You should normally have a single **engine** object for your whole application and re-use it everywhere. @@ -364,45 +174,7 @@ SQLite supports a special database that lives all *in memory*. Hence, it's very * `sqlite://` -//// tab | Python 3.10+ - -```Python hl_lines="11-12 14" -{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py[ln:1-16]!} - -# More code here later 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="13-14 16" -{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-18]!} - -# More code here later 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/create_db_and_table/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py ln[1:16] hl[11:12,14] *} You can read a lot more about all the databases supported by **SQLAlchemy** (and that way supported by **SQLModel**) in the SQLAlchemy documentation. @@ -414,45 +186,7 @@ It will make the engine print all the SQL statements it executes, which can help It is particularly useful for **learning** and **debugging**: -//// tab | Python 3.10+ - -```Python hl_lines="14" -{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py[ln:1-16]!} - -# More code here later 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="16" -{!./docs_src/tutorial/create_db_and_table/tutorial001.py[ln:1-18]!} - -# More code here later 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/create_db_and_table/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py ln[1:16] hl[14] *} But in production, you would probably want to remove `echo=True`: @@ -478,21 +212,7 @@ And SQLModel's version of `create_engine()` is type annotated internally, so you Now everything is in place to finally create the database and table: -//// tab | Python 3.10+ - -```Python hl_lines="16" -{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="18" -{!./docs_src/tutorial/create_db_and_table/tutorial001.py!} -``` - -//// +{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py hl[16] *} /// tip @@ -603,25 +323,7 @@ Let's run the program to see it all working. Put the code it in a file `app.py` if you haven't already. -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/create_db_and_table/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/create_db_and_table/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/create_db_and_table/tutorial001_py310.py *} /// tip @@ -726,45 +428,7 @@ In this example it's just the `SQLModel.metadata.create_all(engine)`. Let's put it in a function `create_db_and_tables()`: -//// tab | Python 3.10+ - -```Python hl_lines="17-18" -{!./docs_src/tutorial/create_db_and_table/tutorial002_py310.py[ln:1-18]!} - -# More code here later 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="19-20" -{!./docs_src/tutorial/create_db_and_table/tutorial002.py[ln:1-20]!} - -# More code here later 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/create_db_and_table/tutorial002_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/create_db_and_table/tutorial002.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/create_db_and_table/tutorial002_py310.py ln[1:18] hl[17:18] *} If `SQLModel.metadata.create_all(engine)` was not in a function and we tried to import something from this module (from this file) in another, it would try to create the database and table **every time** we executed that other file that imported this module. @@ -794,21 +458,7 @@ The word **script** often implies that the code could be run independently and e For that we can use the special variable `__name__` in an `if` block: -//// tab | Python 3.10+ - -```Python hl_lines="21-22" -{!./docs_src/tutorial/create_db_and_table/tutorial002_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="23-24" -{!./docs_src/tutorial/create_db_and_table/tutorial002.py!} -``` - -//// +{* ./docs_src/tutorial/create_db_and_table/tutorial002_py310.py hl[21:22] *} ### About `__name__ == "__main__"` From 3d8a870da4e3944a8db22316e73cebcca26a6fca Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 26 Oct 2024 22:05:26 +0000 Subject: [PATCH 535/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index ee71cfe1af..aab42ce771 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Docs +* 📝 Update includes for `docs/tutorial/create-db-and-table.md`. PR [#1149](https://github.com/fastapi/sqlmodel/pull/1149) by [@tiangolo](https://github.com/tiangolo). * 📝 Fix internal links in docs. PR [#1148](https://github.com/fastapi/sqlmodel/pull/1148) by [@tiangolo](https://github.com/tiangolo). * ✏️ Fix typo in documentation. PR [#1106](https://github.com/fastapi/sqlmodel/pull/1106) by [@Solipsistmonkey](https://github.com/Solipsistmonkey). * 📝 Remove highlights in `indexes.md` . PR [#1100](https://github.com/fastapi/sqlmodel/pull/1100) by [@alejsdev](https://github.com/alejsdev). From a3cfa7618d728a7732c3d34725eadb31434bbf3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 27 Oct 2024 00:55:33 +0200 Subject: [PATCH 536/906] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20markdown?= =?UTF-8?q?-include-variants=20to=20version=200.0.3=20(#1152)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 73fd0ac155..4e8c91466e 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -16,4 +16,4 @@ cairosvg==2.7.1 # For griffe, it formats with black typer == 0.12.3 mkdocs-macros-plugin==1.0.5 -markdown-include-variants==0.0.1 +markdown-include-variants==0.0.3 From 06de217329fb0fae34a54f04588e812d36e641fb Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 26 Oct 2024 22:55:50 +0000 Subject: [PATCH 537/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index aab42ce771..f5332897bf 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -15,6 +15,7 @@ ### Internal +* ⬆️ Upgrade markdown-include-variants to version 0.0.3. PR [#1152](https://github.com/fastapi/sqlmodel/pull/1152) by [@tiangolo](https://github.com/tiangolo). * 👷 Update issue manager workflow. PR [#1137](https://github.com/fastapi/sqlmodel/pull/1137) by [@alejsdev](https://github.com/alejsdev). * 👷 Fix smokeshow, checkout files on CI. PR [#1136](https://github.com/fastapi/sqlmodel/pull/1136) by [@tiangolo](https://github.com/tiangolo). * 👷 Use uv in CI. PR [#1135](https://github.com/fastapi/sqlmodel/pull/1135) by [@tiangolo](https://github.com/tiangolo). From 4f9b2ea9eb8a283c7bbf21c43025fd022f744c61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 27 Oct 2024 09:08:30 +0100 Subject: [PATCH 538/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20includes=20for?= =?UTF-8?q?=20`docs/advanced/uuid.md`=20(#1151)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/advanced/uuid.md | 172 +----------------------------------------- 1 file changed, 4 insertions(+), 168 deletions(-) diff --git a/docs/advanced/uuid.md b/docs/advanced/uuid.md index 56492636f6..abfcfeda2b 100644 --- a/docs/advanced/uuid.md +++ b/docs/advanced/uuid.md @@ -80,45 +80,7 @@ We don't call `uuid.uuid4()` ourselves in the code (we don't put the parenthesis This means that the UUID will be generated in the Python code, **before sending the data to the database**. -//// tab | Python 3.10+ - -```Python hl_lines="1 7" -{!./docs_src/advanced/uuid/tutorial001_py310.py[ln:1-10]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="1 8" -{!./docs_src/advanced/uuid/tutorial001.py[ln:1-11]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/advanced/uuid/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/advanced/uuid/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/advanced/uuid/tutorial001_py310.py ln[1:10] hl[1,7] *} Pydantic has support for `UUID` types. @@ -132,49 +94,7 @@ As `uuid.uuid4` will be called when creating the model instance, even before sen And that **same ID (a UUID)** will be saved in the database. -//// tab | Python 3.10+ - -```Python hl_lines="5 7 9 14" -# Code above omitted 👆 - -{!./docs_src/advanced/uuid/tutorial001_py310.py[ln:23-34]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="5 7 9 14" -# Code above omitted 👆 - -{!./docs_src/advanced/uuid/tutorial001.py[ln:24-35]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/advanced/uuid/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/advanced/uuid/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/advanced/uuid/tutorial001_py310.py ln[23:34] hl[25,27,29,34] *} ### Select a Hero @@ -182,49 +102,7 @@ We can do the same operations we could do with other fields. For example we can **select a hero by ID**: -//// tab | Python 3.10+ - -```Python hl_lines="15" -# Code above omitted 👆 - -{!./docs_src/advanced/uuid/tutorial001_py310.py[ln:37-54]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="15" -# Code above omitted 👆 - -{!./docs_src/advanced/uuid/tutorial001.py[ln:38-55]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/advanced/uuid/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/advanced/uuid/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/advanced/uuid/tutorial001_py310.py ln[37:54] hl[49] *} /// tip @@ -238,49 +116,7 @@ SQLModel (actually SQLAlchemy) will take care of making it work. ✨ We could also select by ID with `session.get()`: -//// tab | Python 3.10+ - -```Python hl_lines="15" -# Code above omitted 👆 - -{!./docs_src/advanced/uuid/tutorial002_py310.py[ln:37-54]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="15" -# Code above omitted 👆 - -{!./docs_src/advanced/uuid/tutorial002.py[ln:38-55]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/advanced/uuid/tutorial002_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/advanced/uuid/tutorial002.py!} -``` - -//// - -/// +{* ./docs_src/advanced/uuid/tutorial002_py310.py ln[37:53] hl[49] *} The same way as with other fields, we could update, delete, etc. 🚀 From 893f8bd0395305d05426ae1e4060c63432fc1d75 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 27 Oct 2024 08:08:55 +0000 Subject: [PATCH 539/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index f5332897bf..581e7cc97a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Docs +* 📝 Update includes for `docs/advanced/uuid.md`. PR [#1151](https://github.com/fastapi/sqlmodel/pull/1151) by [@tiangolo](https://github.com/tiangolo). * 📝 Update includes for `docs/tutorial/create-db-and-table.md`. PR [#1149](https://github.com/fastapi/sqlmodel/pull/1149) by [@tiangolo](https://github.com/tiangolo). * 📝 Fix internal links in docs. PR [#1148](https://github.com/fastapi/sqlmodel/pull/1148) by [@tiangolo](https://github.com/tiangolo). * ✏️ Fix typo in documentation. PR [#1106](https://github.com/fastapi/sqlmodel/pull/1106) by [@Solipsistmonkey](https://github.com/Solipsistmonkey). From ab3d19f0ac4cb4f00b1a851a1e2153b962a11db2 Mon Sep 17 00:00:00 2001 From: Evgeniy Lupashin <34810566+PipeKnight@users.noreply.github.com> Date: Fri, 8 Nov 2024 11:27:23 +0200 Subject: [PATCH 540/906] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo=20in=20?= =?UTF-8?q?the=20release=20notes=20of=20v0.0.22=20(#1195)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix typo in release-notes.md --- docs/release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 581e7cc97a..7c59a514a4 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -36,7 +36,7 @@ ### Fixes -* 🐛 Fix support for types with `Optional[Annoated[x, f()]]`, e.g. `id: Optional[pydantic.UUID4]`. PR [#1093](https://github.com/fastapi/sqlmodel/pull/1093) by [@tiangolo](https://github.com/tiangolo). +* 🐛 Fix support for types with `Optional[Annotated[x, f()]]`, e.g. `id: Optional[pydantic.UUID4]`. PR [#1093](https://github.com/fastapi/sqlmodel/pull/1093) by [@tiangolo](https://github.com/tiangolo). ### Docs From e86b5fcc84af715e502e1d9785cf7f4c4b857710 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 8 Nov 2024 09:27:42 +0000 Subject: [PATCH 541/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 7c59a514a4..672f68cb6d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Docs +* ✏️ Fix typo in the release notes of v0.0.22. PR [#1195](https://github.com/fastapi/sqlmodel/pull/1195) by [@PipeKnight](https://github.com/PipeKnight). * 📝 Update includes for `docs/advanced/uuid.md`. PR [#1151](https://github.com/fastapi/sqlmodel/pull/1151) by [@tiangolo](https://github.com/tiangolo). * 📝 Update includes for `docs/tutorial/create-db-and-table.md`. PR [#1149](https://github.com/fastapi/sqlmodel/pull/1149) by [@tiangolo](https://github.com/tiangolo). * 📝 Fix internal links in docs. PR [#1148](https://github.com/fastapi/sqlmodel/pull/1148) by [@tiangolo](https://github.com/tiangolo). From b1be04300665ec33b1aa8cca469818477640026a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 4 Dec 2024 16:11:16 +0100 Subject: [PATCH 542/906] =?UTF-8?q?=F0=9F=94=A7=20Update=20team=20members?= =?UTF-8?q?=20(#1234)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/members.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/data/members.yml b/data/members.yml index 4dba986672..7a51cbc9ac 100644 --- a/data/members.yml +++ b/data/members.yml @@ -1,4 +1,3 @@ members: - login: tiangolo -- login: estebanx64 - login: alejsdev From 94d6b0ecd64a413112310b5d59d54d920cbdf401 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 4 Dec 2024 15:12:55 +0000 Subject: [PATCH 543/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 672f68cb6d..2eff8d3a26 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -17,6 +17,7 @@ ### Internal +* 🔧 Update team members. PR [#1234](https://github.com/fastapi/sqlmodel/pull/1234) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade markdown-include-variants to version 0.0.3. PR [#1152](https://github.com/fastapi/sqlmodel/pull/1152) by [@tiangolo](https://github.com/tiangolo). * 👷 Update issue manager workflow. PR [#1137](https://github.com/fastapi/sqlmodel/pull/1137) by [@alejsdev](https://github.com/alejsdev). * 👷 Fix smokeshow, checkout files on CI. PR [#1136](https://github.com/fastapi/sqlmodel/pull/1136) by [@tiangolo](https://github.com/tiangolo). From aacf9ec19b326a259e45f441789591c0e637d2ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 4 Dec 2024 16:39:41 +0100 Subject: [PATCH 544/906] =?UTF-8?q?=F0=9F=94=A7=20Update=20build-docs=20fi?= =?UTF-8?q?lter=20paths=20(#1235)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index c35a212039..c106798428 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -41,6 +41,7 @@ jobs: - mkdocs.no-insiders.yml - .github/workflows/build-docs.yml - .github/workflows/deploy-docs.yml + - data/** build-docs: needs: From 811fe9ffc99424a89ffb91abb1600aac18463334 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 4 Dec 2024 15:40:01 +0000 Subject: [PATCH 545/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 2eff8d3a26..1f58544c78 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -17,6 +17,7 @@ ### Internal +* 🔧 Update build-docs filter paths. PR [#1235](https://github.com/fastapi/sqlmodel/pull/1235) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update team members. PR [#1234](https://github.com/fastapi/sqlmodel/pull/1234) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade markdown-include-variants to version 0.0.3. PR [#1152](https://github.com/fastapi/sqlmodel/pull/1152) by [@tiangolo](https://github.com/tiangolo). * 👷 Update issue manager workflow. PR [#1137](https://github.com/fastapi/sqlmodel/pull/1137) by [@alejsdev](https://github.com/alejsdev). From 74496c25f62933c242e9b70aa1110732d719d01f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 4 Dec 2024 18:03:13 +0100 Subject: [PATCH 546/906] =?UTF-8?q?=F0=9F=94=A8=20Update=20docs=20previews?= =?UTF-8?q?=20script=20(#1236)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/deploy_docs_status.py | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/scripts/deploy_docs_status.py b/scripts/deploy_docs_status.py index 8cef2f7581..c107db9463 100644 --- a/scripts/deploy_docs_status.py +++ b/scripts/deploy_docs_status.py @@ -2,9 +2,11 @@ import re from github import Github -from pydantic import SecretStr +from pydantic import BaseModel, SecretStr from pydantic_settings import BaseSettings +site_domain = "sqlmodel.tiangolo.com" + class Settings(BaseSettings): github_repository: str @@ -15,7 +17,12 @@ class Settings(BaseSettings): is_done: bool = False -def main(): +class LinkData(BaseModel): + previous_link: str + preview_link: str + + +def main() -> None: logging.basicConfig(level=logging.INFO) settings = Settings() @@ -60,24 +67,31 @@ def main(): docs_files = [f for f in files if f.filename.startswith("docs/")] deploy_url = settings.deploy_url.rstrip("/") - links: list[str] = [] + links: list[LinkData] = [] for f in docs_files: match = re.match(r"docs/(.*)", f.filename) - assert match + if not match: + continue path = match.group(1) if path.endswith("index.md"): - path = path.replace("index.md", "") + use_path = path.replace("index.md", "") else: - path = path.replace(".md", "/") - link = f"{deploy_url}/{path}" + use_path = path.replace(".md", "/") + link = LinkData( + previous_link=f"https://{site_domain}/{use_path}", + preview_link=f"{deploy_url}/{use_path}", + ) links.append(link) - links.sort() + links.sort(key=lambda x: x.preview_link) message = f"📝 Docs preview for commit {settings.commit_sha} at: {deploy_url}" if links: message += "\n\n### Modified Pages\n\n" - message += "\n".join([f"* {link}" for link in links]) + for link in links: + message += f"* {link.preview_link}" + message += f" - ([before]({link.previous_link}))" + message += "\n" print(message) use_pr.as_issue().create_comment(message) From 33b91794242342c20f267a0a65eeb6eb8c0ec098 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 4 Dec 2024 17:03:36 +0000 Subject: [PATCH 547/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 1f58544c78..f0936cd055 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -17,6 +17,7 @@ ### Internal +* 🔨 Update docs previews script. PR [#1236](https://github.com/fastapi/sqlmodel/pull/1236) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update build-docs filter paths. PR [#1235](https://github.com/fastapi/sqlmodel/pull/1235) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update team members. PR [#1234](https://github.com/fastapi/sqlmodel/pull/1234) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade markdown-include-variants to version 0.0.3. PR [#1152](https://github.com/fastapi/sqlmodel/pull/1152) by [@tiangolo](https://github.com/tiangolo). From 9cc1772b648ddecc3582ffea72195e69ae35abe1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 11:26:51 +0000 Subject: [PATCH 548/906] =?UTF-8?q?=E2=AC=86=20Bump=20tiangolo/latest-chan?= =?UTF-8?q?ges=20from=200.3.1=20to=200.3.2=20(#1207)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [tiangolo/latest-changes](https://github.com/tiangolo/latest-changes) from 0.3.1 to 0.3.2. - [Release notes](https://github.com/tiangolo/latest-changes/releases) - [Commits](https://github.com/tiangolo/latest-changes/compare/0.3.1...0.3.2) --- updated-dependencies: - dependency-name: tiangolo/latest-changes dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/latest-changes.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml index 77a3b3eec6..d02ae1d565 100644 --- a/.github/workflows/latest-changes.yml +++ b/.github/workflows/latest-changes.yml @@ -30,7 +30,7 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true - - uses: tiangolo/latest-changes@0.3.1 + - uses: tiangolo/latest-changes@0.3.2 with: token: ${{ secrets.GITHUB_TOKEN }} latest_changes_file: docs/release-notes.md From 2b268d135c4d34208104e1362c288020a772d6f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 11:27:02 +0000 Subject: [PATCH 549/906] =?UTF-8?q?=E2=AC=86=20Bump=20astral-sh/setup-uv?= =?UTF-8?q?=20from=203=20to=204=20(#1225)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 3 to 4. - [Release notes](https://github.com/astral-sh/setup-uv/releases) - [Commits](https://github.com/astral-sh/setup-uv/compare/v3...v4) --- updated-dependencies: - dependency-name: astral-sh/setup-uv dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-docs.yml | 2 +- .github/workflows/deploy-docs.yml | 2 +- .github/workflows/smokeshow.yml | 2 +- .github/workflows/test.yml | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index c106798428..590cf8532b 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -59,7 +59,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@v4 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 6849bd782f..d8634e253f 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -29,7 +29,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@v4 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index 651f838da8..7a1c88ddd9 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -21,7 +21,7 @@ jobs: with: python-version: '3.9' - name: Setup uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@v4 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3509d5d3df..4590e080c3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,7 +44,7 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Setup uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@v4 with: version: "0.4.15" enable-cache: true @@ -92,7 +92,7 @@ jobs: with: python-version: '3.12' - name: Setup uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@v4 with: version: "0.4.15" enable-cache: true From b8a6a1f869207d23f9170a19f01b6a3050539e24 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 11:27:27 +0000 Subject: [PATCH 550/906] =?UTF-8?q?=E2=AC=86=20Bump=20pypa/gh-action-pypi-?= =?UTF-8?q?publish=20from=201.9.0=20to=201.12.3=20(#1240)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.9.0 to 1.12.3. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.9.0...v1.12.3) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 10be182e6b..725928abb3 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -34,4 +34,4 @@ jobs: TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }} run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.9.0 + uses: pypa/gh-action-pypi-publish@v1.12.3 From 0c78e0759add02059ef803175aebe464f6315d39 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 10 Dec 2024 11:29:25 +0000 Subject: [PATCH 551/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index f0936cd055..cbacec900e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -17,6 +17,7 @@ ### Internal +* ⬆ Bump tiangolo/latest-changes from 0.3.1 to 0.3.2. PR [#1207](https://github.com/fastapi/sqlmodel/pull/1207) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔨 Update docs previews script. PR [#1236](https://github.com/fastapi/sqlmodel/pull/1236) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update build-docs filter paths. PR [#1235](https://github.com/fastapi/sqlmodel/pull/1235) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update team members. PR [#1234](https://github.com/fastapi/sqlmodel/pull/1234) by [@tiangolo](https://github.com/tiangolo). From b34a20448b17a4f5a1414a2f4cae2b003bb91174 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 10 Dec 2024 11:29:33 +0000 Subject: [PATCH 552/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index cbacec900e..235299e7d1 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -17,6 +17,7 @@ ### Internal +* ⬆ Bump astral-sh/setup-uv from 3 to 4. PR [#1225](https://github.com/fastapi/sqlmodel/pull/1225) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump tiangolo/latest-changes from 0.3.1 to 0.3.2. PR [#1207](https://github.com/fastapi/sqlmodel/pull/1207) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔨 Update docs previews script. PR [#1236](https://github.com/fastapi/sqlmodel/pull/1236) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update build-docs filter paths. PR [#1235](https://github.com/fastapi/sqlmodel/pull/1235) by [@tiangolo](https://github.com/tiangolo). From 8c275289f228a2834c7285972a5006229f41f3cf Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 10 Dec 2024 11:30:12 +0000 Subject: [PATCH 553/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 235299e7d1..083ce99275 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -17,6 +17,7 @@ ### Internal +* ⬆ Bump pypa/gh-action-pypi-publish from 1.9.0 to 1.12.3. PR [#1240](https://github.com/fastapi/sqlmodel/pull/1240) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump astral-sh/setup-uv from 3 to 4. PR [#1225](https://github.com/fastapi/sqlmodel/pull/1225) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump tiangolo/latest-changes from 0.3.1 to 0.3.2. PR [#1207](https://github.com/fastapi/sqlmodel/pull/1207) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔨 Update docs previews script. PR [#1236](https://github.com/fastapi/sqlmodel/pull/1236) by [@tiangolo](https://github.com/tiangolo). From 3d064c454244422fe31b12d78160058bb3c2de40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 21 Dec 2024 12:14:34 +0000 Subject: [PATCH 554/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20fenced=20code?= =?UTF-8?q?=20in=20Decimal=20docs=20for=20consistency=20(#1251)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/advanced/decimal.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/advanced/decimal.md b/docs/advanced/decimal.md index 26994eccf8..1234dcb83b 100644 --- a/docs/advanced/decimal.md +++ b/docs/advanced/decimal.md @@ -35,7 +35,7 @@ Let's say that each hero in the database will have an amount of money. We could //// tab | Python 3.10+ -```python hl_lines="11" +```Python hl_lines="11" {!./docs_src/advanced/decimal/tutorial001_py310.py[ln:1-11]!} # More code here later 👇 @@ -45,7 +45,7 @@ Let's say that each hero in the database will have an amount of money. We could //// tab | Python 3.7+ -```python hl_lines="12" +```Python hl_lines="12" {!./docs_src/advanced/decimal/tutorial001.py[ln:1-12]!} # More code here later 👇 From 0c65fed61b72c95fdf675c199c8bd45642122e1c Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 21 Dec 2024 12:14:52 +0000 Subject: [PATCH 555/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 083ce99275..6805a0dc31 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Docs +* 📝 Update fenced code in Decimal docs for consistency. PR [#1251](https://github.com/fastapi/sqlmodel/pull/1251) by [@tiangolo](https://github.com/tiangolo). * ✏️ Fix typo in the release notes of v0.0.22. PR [#1195](https://github.com/fastapi/sqlmodel/pull/1195) by [@PipeKnight](https://github.com/PipeKnight). * 📝 Update includes for `docs/advanced/uuid.md`. PR [#1151](https://github.com/fastapi/sqlmodel/pull/1151) by [@tiangolo](https://github.com/tiangolo). * 📝 Update includes for `docs/tutorial/create-db-and-table.md`. PR [#1149](https://github.com/fastapi/sqlmodel/pull/1149) by [@tiangolo](https://github.com/tiangolo). From 5100200beab70261df745386fb86f77eb5eceed4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 22 Dec 2024 14:30:05 +0000 Subject: [PATCH 556/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20markdown=20incl?= =?UTF-8?q?udes=20format=20(#1254)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/advanced/decimal.md | 128 +--- docs/tutorial/automatic-id-none-refresh.md | 352 +-------- .../tutorial/connect/create-connected-rows.md | 240 +----- .../connect/create-connected-tables.md | 160 +--- docs/tutorial/connect/read-connected-data.md | 329 +------- .../connect/remove-data-connections.md | 72 +- .../connect/update-data-connections.md | 72 +- docs/tutorial/delete.md | 324 +------- docs/tutorial/fastapi/delete.md | 64 +- docs/tutorial/fastapi/limit-and-offset.md | 70 +- docs/tutorial/fastapi/multiple-models.md | 562 +------------- docs/tutorial/fastapi/read-one.md | 192 +---- docs/tutorial/fastapi/relationships.md | 316 +------- docs/tutorial/fastapi/response-model.md | 132 +--- .../fastapi/session-with-dependency.md | 450 +---------- docs/tutorial/fastapi/simple-hero-api.md | 225 +----- docs/tutorial/fastapi/teams.md | 250 +----- docs/tutorial/fastapi/tests.md | 56 +- docs/tutorial/fastapi/update-extra-data.md | 332 +------- docs/tutorial/fastapi/update.md | 320 +------- docs/tutorial/indexes.md | 184 +---- docs/tutorial/insert.md | 374 +-------- docs/tutorial/limit-and-offset.md | 264 +------ docs/tutorial/many-to-many/create-data.md | 220 +----- .../many-to-many/create-models-with-link.md | 319 +------- .../many-to-many/link-with-extra-fields.md | 396 +--------- .../update-remove-relationships.md | 284 +------ docs/tutorial/one.md | 460 +---------- .../relationship-attributes/back-populates.md | 722 +----------------- .../cascade-delete-relationships.md | 675 +--------------- .../create-and-update-relationships.md | 336 +------- .../define-relationships-attributes.md | 156 +--- .../read-relationships.md | 280 +------ .../remove-relationships.md | 128 +--- .../type-annotation-strings.md | 58 +- docs/tutorial/select.md | 372 +-------- docs/tutorial/update.md | 324 +------- docs/tutorial/where.md | 672 +--------------- requirements-docs.txt | 2 +- 39 files changed, 213 insertions(+), 10659 deletions(-) diff --git a/docs/advanced/decimal.md b/docs/advanced/decimal.md index 1234dcb83b..ce971b201b 100644 --- a/docs/advanced/decimal.md +++ b/docs/advanced/decimal.md @@ -33,45 +33,7 @@ For the database, **SQLModel** will use FastAPI uses the `response_model` to validate and **filter** the response data? In this case, we used `response_model=TeamPublic` and `response_model=HeroPublic`, so FastAPI will use them to filter the response data, even if we return a **table model** that includes **relationship attributes**: -//// tab | Python 3.10+ - -```Python hl_lines="3 8 12 17" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py[ln:102-107]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py[ln:156-161]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="3 8 12 17" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py[ln:104-109]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py[ln:158-163]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="3 8 12 17" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:104-109]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:158-163]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/fastapi/teams/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/fastapi/teams/tutorial001_py310.py ln[102:107,156:161] hl[102,107,156,161] *} ## Don't Include All the Data @@ -304,69 +132,7 @@ Let's add the models `HeroPublicWithTeam` and `TeamPublicWithHeroes`. We'll add them **after** the other models so that we can easily reference the previous models. -//// tab | Python 3.10+ - -```Python hl_lines="3-4 7-8" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/relationships/tutorial001_py310.py[ln:59-64]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="3-4 7-8" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/relationships/tutorial001_py39.py[ln:61-66]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="3-4 7-8" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/relationships/tutorial001.py[ln:61-66]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/fastapi/relationships/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/fastapi/relationships/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/fastapi/relationships/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/fastapi/relationships/tutorial001_py310.py ln[59:64] hl[59:60,63:64] *} These two models are very **simple in code**, but there's a lot happening here. Let's check it out. @@ -400,81 +166,7 @@ This will tell **FastAPI** to take the object that we return from the *path oper In the case of the hero, this tells FastAPI to extract the `team` too. And in the case of the team, to extract the list of `heroes` too. -//// tab | Python 3.10+ - -```Python hl_lines="3 8 12 17" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/relationships/tutorial001_py310.py[ln:111-116]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/relationships/tutorial001_py310.py[ln:165-170]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="3 8 12 17" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/relationships/tutorial001_py39.py[ln:113-118]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/relationships/tutorial001_py39.py[ln:167-172]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="3 8 12 17" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/relationships/tutorial001.py[ln:113-118]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/relationships/tutorial001.py[ln:167-172]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/fastapi/relationships/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/fastapi/relationships/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/fastapi/relationships/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/fastapi/relationships/tutorial001_py310.py ln[111:116,165:170] hl[111,116,165,170] *} ## Check It Out in the Docs UI diff --git a/docs/tutorial/fastapi/response-model.md b/docs/tutorial/fastapi/response-model.md index b333d58b1a..f5c0ab9f95 100644 --- a/docs/tutorial/fastapi/response-model.md +++ b/docs/tutorial/fastapi/response-model.md @@ -32,69 +32,7 @@ We can use `response_model` to tell FastAPI the schema of the data we want to se For example, we can pass the same `Hero` **SQLModel** class (because it is also a Pydantic model): -//// tab | Python 3.10+ - -```Python hl_lines="3" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/response_model/tutorial001_py310.py[ln:31-37]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="3" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/response_model/tutorial001_py39.py[ln:33-39]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="3" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/response_model/tutorial001.py[ln:33-39]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/fastapi/response_model/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/fastapi/response_model/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/fastapi/response_model/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/fastapi/response_model/tutorial001_py310.py ln[31:37] hl[31] *} ## List of Heroes in `response_model` @@ -102,73 +40,7 @@ We can also use other type annotations, the same way we can use with Pydantic fi First, we import `List` from `typing` and then we declare the `response_model` with `List[Hero]`: -//// tab | Python 3.10+ - -```Python hl_lines="3" - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/response_model/tutorial001_py310.py[ln:40-44]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="3" - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/response_model/tutorial001_py39.py[ln:42-46]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="1 5" -{!./docs_src/tutorial/fastapi/response_model/tutorial001.py[ln:1]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/response_model/tutorial001.py[ln:42-46]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/fastapi/response_model/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/fastapi/response_model/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/fastapi/response_model/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/fastapi/response_model/tutorial001_py310.py ln[40:44] hl[40] *} ## FastAPI and Response Model diff --git a/docs/tutorial/fastapi/session-with-dependency.md b/docs/tutorial/fastapi/session-with-dependency.md index e148452dcb..e81e9e6745 100644 --- a/docs/tutorial/fastapi/session-with-dependency.md +++ b/docs/tutorial/fastapi/session-with-dependency.md @@ -6,69 +6,7 @@ Before we keep adding things, let's change a bit how we get the session for each Up to now, we have been creating a session in each *path operation*, in a `with` block. -//// tab | Python 3.10+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/delete/tutorial001_py310.py[ln:48-55]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/delete/tutorial001_py39.py[ln:50-57]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/delete/tutorial001.py[ln:50-57]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/fastapi/delete/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/fastapi/delete/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/fastapi/delete/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/fastapi/delete/tutorial001_py310.py ln[48:55] hl[50] *} That's perfectly fine, but in many use cases we would want to use FastAPI Dependencies, for example to **verify** that the client is **logged in** and get the **current user** before executing any other code in the *path operation*. @@ -82,69 +20,7 @@ A **FastAPI** dependency is very simple, it's just a function that returns a val It could use `yield` instead of `return`, and in that case **FastAPI** will make sure it executes all the code **after** the `yield`, once it is done with the request. -//// tab | Python 3.10+ - -```Python hl_lines="3-5" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:40-42]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="3-5" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:42-44]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="3-5" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:42-44]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py ln[40:42] hl[40:42] *} ## Use the Dependency @@ -152,87 +28,7 @@ Now let's make FastAPI execute a dependency and get its value in the *path opera We import `Depends()` from `fastapi`. Then we use it in the *path operation function* in a **parameter**, the same way we declared parameters to get JSON bodies, path parameters, etc. -//// tab | Python 3.10+ - -```Python hl_lines="1 13" -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:1-2]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:40-42]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:53-59]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="3 15" -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:1-4]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:42-44]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:55-61]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="3 15" -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:1-4]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:42-44]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:55-61]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py ln[1:2,40:42,53:59] hl[1,54] *} /// tip @@ -260,173 +56,13 @@ And because dependencies can use `yield`, FastAPI will make sure to run the code This means that in the main code of the *path operation function*, it will work equivalently to the previous version with the explicit `with` block. -//// tab | Python 3.10+ - -```Python hl_lines="14-18" -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:1-2]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:40-42]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:53-59]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="16-20" -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:1-4]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:42-44]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:55-61]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="16-20" -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:1-4]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:42-44]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:55-61]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py ln[1:2,40:42,53:59] hl[55:59] *} In fact, you could think that all that block of code inside of the `create_hero()` function is still inside a `with` block for the **session**, because this is more or less what's happening behind the scenes. But now, the `with` block is not explicitly in the function, but in the dependency above: -//// tab | Python 3.10+ - -```Python hl_lines="7-8" -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:1-2]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:40-42]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:53-59]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="9-10" -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:1-4]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:42-44]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:55-61]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="9-10" -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:1-4]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:42-44]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:55-61]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py ln[1:2,40:42,53:59] hl[41:42] *} We will see how this is very useful when testing the code later. ✅ @@ -442,81 +78,7 @@ session: Session = Depends(get_session) And then we remove the previous `with` block with the old **session**. -//// tab | Python 3.10+ - -```Python hl_lines="13 24 33 42 57" -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:1-2]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:40-42]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py[ln:53-104]!} -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="15 26 35 44 59" -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:1-4]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:42-44]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py[ln:55-106]!} -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="15 26 35 44 59" -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:1-4]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:42-44]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:55-106]!} -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py ln[1:2,40:42,53:104] hl[54,65,74,83,98] *} ## Recap diff --git a/docs/tutorial/fastapi/simple-hero-api.md b/docs/tutorial/fastapi/simple-hero-api.md index fc85f2a008..c6f27f4baf 100644 --- a/docs/tutorial/fastapi/simple-hero-api.md +++ b/docs/tutorial/fastapi/simple-hero-api.md @@ -32,54 +32,7 @@ We will start with the **simplest version**, with just heroes (no teams yet). This is almost the same code we have seen up to now in previous examples: -//// tab | Python 3.10+ - -```Python hl_lines="18-19" - -# One line of FastAPI imports here later 👈 -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py[ln:2]!} - -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py[ln:5-20]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="20-21" -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py[ln:1]!} - -# One line of FastAPI imports here later 👈 -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py[ln:4]!} - -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py[ln:7-22]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py ln[2,5:20] hl[19:20] *} There's only one change here from the code we have used before, the `check_same_thread` in the `connect_args`. @@ -107,53 +60,7 @@ We will import the `FastAPI` class from `fastapi`. And then create an `app` object that is an instance of that `FastAPI` class: -//// tab | Python 3.10+ - -```Python hl_lines="1 6" -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py[ln:1-2]!} - -# SQLModel code here omitted 👈 - -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py[ln:23]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="3 8" -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py[ln:1-4]!} - -# SQLModel code here omitted 👈 - -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py[ln:25]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py ln[1:2,23] hl[1,23] *} ## Create Database and Tables on `startup` @@ -161,49 +68,7 @@ We want to make sure that once the app starts running, the function `create_tabl This should be called only once at startup, not before every request, so we put it in the function to handle the `"startup"` event: -//// tab | Python 3.10+ - -```Python hl_lines="6-8" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py[ln:23-28]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="6-8" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py[ln:25-30]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py ln[23:28] hl[26:28] *} ## Create Heroes *Path Operation* @@ -217,49 +82,7 @@ Let's create the **path operation** code to create a new hero. It will be called when a user sends a request with a `POST` **operation** to the `/heroes/` **path**: -//// tab | Python 3.10+ - -```Python hl_lines="11-12" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py[ln:23-37]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="11-12" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py[ln:25-39]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py ln[23:37] hl[31:32] *} /// info @@ -293,45 +116,7 @@ We will improve this further later, but for now, it already shows the power of h Now let's add another **path operation** to read all the heroes: -//// tab | Python 3.10+ - -```Python hl_lines="20-24" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py[ln:23-44]!} -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="20-24" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py[ln:25-46]!} -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py310.py ln[23:44] hl[40:44] *} This is pretty straightforward. diff --git a/docs/tutorial/fastapi/teams.md b/docs/tutorial/fastapi/teams.md index 9e366f2cc2..93d38829f7 100644 --- a/docs/tutorial/fastapi/teams.md +++ b/docs/tutorial/fastapi/teams.md @@ -18,63 +18,7 @@ Then we also inherit from the `TeamBase` for the `TeamCreate` and `TeamPublic` * And we also create a `TeamUpdate` **data model**. -//// tab | Python 3.10+ - -```Python hl_lines="5-7 10-13 16-17 20-21 24-26" -{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py[ln:1-26]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="7-9 12-15 18-19 22-23 26-28" -{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py[ln:1-28]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="7-9 12-15 18-19 22-23 26-28" -{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:1-28]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/fastapi/teams/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/fastapi/teams/tutorial001_py310.py ln[1:26] hl[5:7,10:13,16:17,20:21,24:26] *} We now also have **relationship attributes**. 🎉 @@ -82,69 +26,7 @@ Let's now update the `Hero` models too. ## Update Hero Models -//// tab | Python 3.10+ - -```Python hl_lines="3-8 11-14 17-18 21-22 25-29" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py[ln:29-55]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="3-8 11-14 17-18 21-22 25-29" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py[ln:31-57]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="3-8 11-14 17-18 21-22 25-29" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:31-57]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/fastapi/teams/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/fastapi/teams/tutorial001_py310.py ln[29:55] hl[29:34,37:40,43:44,47:48,51:55] *} We now have a `team_id` in the hero models. @@ -156,69 +38,7 @@ And even though the `HeroBase` is *not* a **table model**, we can declare `team_ Notice that the **relationship attributes**, the ones with `Relationship()`, are **only** in the **table models**, as those are the ones that are handled by **SQLModel** with SQLAlchemy and that can have the automatic fetching of data from the database when we access them. -//// tab | Python 3.10+ - -```Python hl_lines="11 38" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py[ln:5-55]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="11 38" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py[ln:7-57]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="11 38" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:7-57]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/fastapi/teams/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/fastapi/teams/tutorial001_py310.py ln[5:55] hl[13,40] *} ## Path Operations for Teams @@ -226,69 +46,7 @@ Let's now add the **path operations** for teams. These are equivalent and very similar to the **path operations** for the **heroes** we had before, so we don't have to go over the details for each one, let's check the code. -//// tab | Python 3.10+ - -```Python hl_lines="3-9 12-20 23-28 31-47 50-57" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py[ln:136-190]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="3-9 12-20 23-28 31-47 50-57" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py[ln:138-192]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="3-9 12-20 23-28 31-47 50-57" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:138-192]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/fastapi/teams/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/fastapi/teams/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/fastapi/teams/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/fastapi/teams/tutorial001_py310.py ln[136:190] hl[136:142,145:153,156:161,164:180,183:190] *} ## Using Relationships Attributes diff --git a/docs/tutorial/fastapi/tests.md b/docs/tutorial/fastapi/tests.md index 922103bdb8..9c5255b7bf 100644 --- a/docs/tutorial/fastapi/tests.md +++ b/docs/tutorial/fastapi/tests.md @@ -14,13 +14,7 @@ We will use the application with the hero models, but without team models, and w Now we will see how useful it is to have this session dependency. ✨ -/// details | 👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/main.py!} -``` - -/// +{* ./docs_src/tutorial/fastapi/app_testing/tutorial001/main.py ln[0] *} ## File Structure @@ -304,21 +298,7 @@ But normally we will create **lots of other test functions**. And now all the bo Let's add some more tests: -```Python hl_lines="3 22" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main.py[ln:30-58]!} - -# Code below omitted 👇 -``` - -/// details | 👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main.py!} -``` - -/// +{* ./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main.py ln[30:58] hl[30,49] *} /// tip @@ -338,23 +318,7 @@ For these examples, **that would have been simpler**, there's no need to separat But for the next test function, we will require **both fixtures**, the **client** and the **session**. -```Python hl_lines="6 10" -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main.py[ln:1-6]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main.py[ln:61-81]!} - -# Code below omitted 👇 -``` - -/// details | 👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main.py!} -``` - -/// +{* ./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main.py ln[1:6,61:81] hl[6,61] *} In this test function, we want to check that the *path operation* to **read a list of heroes** actually sends us heroes. @@ -380,19 +344,7 @@ The function for the **client fixture** and the actual testing function will **b Using the same ideas, requiring the fixtures, creating data that we need for the tests, etc., we can now add the rest of the tests. They look quite similar to what we have done up to now. -```Python hl_lines="3 18 33" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main.py[ln:84-125]!} -``` - -/// details | 👀 Full file preview - -```Python -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main.py!} -``` - -/// +{* ./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main.py ln[84:125] hl[84,99,114] *} ## Run the Tests diff --git a/docs/tutorial/fastapi/update-extra-data.md b/docs/tutorial/fastapi/update-extra-data.md index 6bbd72ad33..95843144ae 100644 --- a/docs/tutorial/fastapi/update-extra-data.md +++ b/docs/tutorial/fastapi/update-extra-data.md @@ -38,69 +38,7 @@ The `Hero` table model will now store a new field `hashed_password`. And the data models for `HeroCreate` and `HeroUpdate` will also have a new field `password` that will contain the plain text password sent by clients. -//// tab | Python 3.10+ - -```Python hl_lines="11 15 26" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial002_py310.py[ln:5-28]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="11 15 26" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial002_py39.py[ln:7-30]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="11 15 26" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial002.py[ln:7-30]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/fastapi/update/tutorial002_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/fastapi/update/tutorial002_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/fastapi/update/tutorial002.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/fastapi/update/tutorial002_py310.py ln[5:28] hl[13,17,28] *} When a client is creating a new hero, they will send the `password` in the request body. @@ -112,81 +50,7 @@ The app will receive the data from the client using the `HeroCreate` model. This contains the `password` field with the plain text password, and we cannot use that one. So we need to generate a hash from it. -//// tab | Python 3.10+ - -```Python hl_lines="11" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial002_py310.py[ln:42-44]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/update/tutorial002_py310.py[ln:55-57]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="11" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial002_py39.py[ln:44-46]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/update/tutorial002_py39.py[ln:57-59]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="11" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial002.py[ln:44-46]!} - -# Code here omitted 👈 - -{!./docs_src/tutorial/fastapi/update/tutorial002.py[ln:57-59]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/fastapi/update/tutorial002_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/fastapi/update/tutorial002_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/fastapi/update/tutorial002.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/fastapi/update/tutorial002_py310.py ln[42:44,55:57] hl[57] *} ## Create an Object with Extra Data @@ -242,69 +106,7 @@ So now, `db_user_dict` has the updated `age` field with `32` instead of `None` a Similar to how dictionaries have an `update` method, **SQLModel** models have a parameter `update` in `Hero.model_validate()` that takes a dictionary with extra data, or data that should take precedence: -//// tab | Python 3.10+ - -```Python hl_lines="8" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial002_py310.py[ln:55-64]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="8" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial002_py39.py[ln:57-66]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="8" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial002.py[ln:57-66]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/fastapi/update/tutorial002_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/fastapi/update/tutorial002_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/fastapi/update/tutorial002.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/fastapi/update/tutorial002_py310.py ln[55:64] hl[60] *} Now, `db_hero` (which is a *table model* `Hero`) will extract its values from `hero` (which is a *data model* `HeroCreate`), and then it will **`update`** its values with the extra data from the dictionary `extra_data`. @@ -318,69 +120,7 @@ Now let's say we want to **update a hero** that already exists in the database. The same way as before, to avoid removing existing data, we will use `exclude_unset=True` when calling `hero.model_dump()`, to get a dictionary with only the data sent by the client. -//// tab | Python 3.10+ - -```Python hl_lines="9" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial002_py310.py[ln:83-89]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="9" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial002_py39.py[ln:85-91]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="9" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial002.py[ln:85-91]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/fastapi/update/tutorial002_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/fastapi/update/tutorial002_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/fastapi/update/tutorial002.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/fastapi/update/tutorial002_py310.py ln[83:89] hl[89] *} Now, this `hero_data` dictionary could contain a `password`. We need to check it, and if it's there, we need to generate the `hashed_password`. @@ -390,69 +130,7 @@ And then we can update the `db_hero` object using the method `db_hero.sqlmodel_u It takes a model object or dictionary with the data to update the object and also an **additional `update` argument** with extra data. -//// tab | Python 3.10+ - -```Python hl_lines="15" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial002_py310.py[ln:83-99]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="15" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial002_py39.py[ln:85-101]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="15" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial002.py[ln:85-101]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/fastapi/update/tutorial002_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/fastapi/update/tutorial002_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/fastapi/update/tutorial002.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/fastapi/update/tutorial002_py310.py ln[83:99] hl[95] *} /// tip diff --git a/docs/tutorial/fastapi/update.md b/docs/tutorial/fastapi/update.md index 6b2411bea8..0aed115d6d 100644 --- a/docs/tutorial/fastapi/update.md +++ b/docs/tutorial/fastapi/update.md @@ -22,69 +22,7 @@ Because each field is **actually different** (we just change it to `Optional`, b So, let's create this new `HeroUpdate` model: -//// tab | Python 3.10+ - -```Python hl_lines="21-24" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial001_py310.py[ln:5-26]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="21-24" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial001_py39.py[ln:7-28]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="21-24" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial001.py[ln:7-28]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/fastapi/update/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/fastapi/update/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/fastapi/update/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/fastapi/update/tutorial001_py310.py ln[5:26] hl[23:26] *} This is almost the same as `HeroBase`, but all the fields are optional, so we can't simply inherit from `HeroBase`. @@ -94,69 +32,7 @@ Now let's use this model in the *path operation* to update a hero. We will use a `PATCH` HTTP operation. This is used to **partially update data**, which is what we are doing. -//// tab | Python 3.10+ - -```Python hl_lines="3-4" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial001_py310.py[ln:74-89]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="3-4" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial001_py39.py[ln:76-91]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="3-4" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial001.py[ln:76-91]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/fastapi/update/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/fastapi/update/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/fastapi/update/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/fastapi/update/tutorial001_py310.py ln[74:89] hl[74:75] *} We also read the `hero_id` from the *path parameter* and the request body, a `HeroUpdate`. @@ -166,69 +42,7 @@ We take a `hero_id` with the **ID** of the hero **we want to update**. So, we need to read the hero from the database, with the **same logic** we used to **read a single hero**, checking if it exists, possibly raising an error for the client if it doesn't exist, etc. -//// tab | Python 3.10+ - -```Python hl_lines="6-8" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial001_py310.py[ln:74-89]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="6-8" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial001_py39.py[ln:76-91]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="6-8" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial001.py[ln:76-91]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/fastapi/update/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/fastapi/update/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/fastapi/update/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/fastapi/update/tutorial001_py310.py ln[74:89] hl[77:79] *} ### Get the New Data @@ -280,69 +94,7 @@ Then the dictionary we would get in Python using `hero.model_dump(exclude_unset= Then we use that to get the data that was actually sent by the client: -//// tab | Python 3.10+ - -```Python hl_lines="9" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial001_py310.py[ln:74-89]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="9" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial001_py39.py[ln:76-91]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="9" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial001.py[ln:76-91]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/fastapi/update/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/fastapi/update/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/fastapi/update/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/fastapi/update/tutorial001_py310.py ln[74:89] hl[80] *} /// tip Before SQLModel 0.0.14, the method was called `hero.dict(exclude_unset=True)`, but it was renamed to `hero.model_dump(exclude_unset=True)` to be consistent with Pydantic v2. @@ -352,69 +104,7 @@ Before SQLModel 0.0.14, the method was called `hero.dict(exclude_unset=True)`, b Now that we have a **dictionary with the data sent by the client**, we can use the method `db_hero.sqlmodel_update()` to update the object `db_hero`. -//// tab | Python 3.10+ - -```Python hl_lines="10" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial001_py310.py[ln:74-89]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="10" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial001_py39.py[ln:76-91]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="10" -# Code above omitted 👆 - -{!./docs_src/tutorial/fastapi/update/tutorial001.py[ln:76-91]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/fastapi/update/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/fastapi/update/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/fastapi/update/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/fastapi/update/tutorial001_py310.py ln[74:89] hl[81] *} /// tip diff --git a/docs/tutorial/indexes.md b/docs/tutorial/indexes.md index fea4289a44..80314ef274 100644 --- a/docs/tutorial/indexes.md +++ b/docs/tutorial/indexes.md @@ -20,25 +20,7 @@ Are you already a **SQL expert** and don't have time for all my explanations? Fine, in that case, you can **sneak peek** the final code to create indexes here. -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/indexes/tutorial002_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/indexes/tutorial002.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/indexes/tutorial002_py310.py ln[0] *} ..but if you are not an expert, **continue reading**, this will probably be useful. 🤓 @@ -275,87 +257,11 @@ The change in code is underwhelming, it's very simple. 😆 Here's the `Hero` model we had before: -//// tab | Python 3.10+ - -```Python hl_lines="6" -{!./docs_src/tutorial/where/tutorial001_py310.py[ln:1-8]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="8" -{!./docs_src/tutorial/where/tutorial001.py[ln:1-10]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/where/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/where/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/where/tutorial001_py310.py ln[1:8] hl[6] *} Let's now update it to tell **SQLModel** to create an index for the `name` field when creating the table: -//// tab | Python 3.10+ - -```Python hl_lines="6" -{!./docs_src/tutorial/indexes/tutorial001_py310.py[ln:1-8]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="8" -{!./docs_src/tutorial/indexes/tutorial001.py[ln:1-10]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/indexes/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/indexes/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/indexes/tutorial001_py310.py ln[1:8] hl[6] *} We use the same `Field()` again as we did before, and set `index=True`. That's it! 🚀 @@ -377,49 +283,7 @@ The SQL database will figure it out **automatically**. ✨ This is great because it means that indexes are very **simple to use**. But it might also feel counterintuitive at first, as you are **not doing anything** explicitly in the code to make it obvious that the index is useful, it all happens in the database behind the scenes. -//// tab | Python 3.10+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/indexes/tutorial001_py310.py[ln:34-39]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/indexes/tutorial001.py[ln:36-41]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/indexes/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/indexes/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/indexes/tutorial001_py310.py ln[34:39] hl[36] *} This is exactly the same code as we had before, but now the database will **use the index** underneath. @@ -462,45 +326,7 @@ secret_name='Dive Wilson' age=None id=1 name='Deadpond' We are going to query the `hero` table doing comparisons on the `age` field too, so we should **define an index** for that one as well: -//// tab | Python 3.10+ - -```Python hl_lines="8" -{!./docs_src/tutorial/indexes/tutorial002_py310.py[ln:1-8]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="10" -{!./docs_src/tutorial/indexes/tutorial002.py[ln:1-10]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/indexes/tutorial002_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/indexes/tutorial002.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/indexes/tutorial002_py310.py ln[1:8] hl[8] *} In this case, we want the default value of `age` to continue being `None`, so we set `default=None` when using `Field()`. diff --git a/docs/tutorial/insert.md b/docs/tutorial/insert.md index 9439cc3774..272287e624 100644 --- a/docs/tutorial/insert.md +++ b/docs/tutorial/insert.md @@ -145,49 +145,7 @@ So, the first step is to simply create an instance of `Hero`. We'll create 3 right away, for the 3 heroes: -//// tab | Python 3.10+ - -```Python -# Code above omitted 👆 - -{!./docs_src/tutorial/insert/tutorial002_py310.py[ln:21-24]!} - -# More code here later 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python -# Code above omitted 👆 - -{!./docs_src/tutorial/insert/tutorial002.py[ln:23-26]!} - -# More code here later 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/insert/tutorial002_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/insert/tutorial002.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/insert/tutorial002_py310.py ln[21:24] *} /// tip @@ -219,91 +177,11 @@ We would re-use the same **engine** in all the code, everywhere in the applicati The first step is to import the `Session` class: -//// tab | Python 3.10+ - -```Python hl_lines="1" -{!./docs_src/tutorial/insert/tutorial001_py310.py[ln:1]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="3" -{!./docs_src/tutorial/insert/tutorial001.py[ln:1-3]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/insert/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/insert/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/insert/tutorial001_py310.py ln[1] hl[1] *} Then we can create a new session: -//// tab | Python 3.10+ - -```Python hl_lines="8" -# Code above omitted 👆 - -{!./docs_src/tutorial/insert/tutorial001_py310.py[ln:21-26]!} - -# More code here later 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="8" -# Code above omitted 👆 - -{!./docs_src/tutorial/insert/tutorial001.py[ln:23-28]!} - -# More code here later 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/insert/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/insert/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/insert/tutorial001_py310.py ln[21:26] hl[26] *} The new `Session` takes an `engine` as a parameter. And it will use the **engine** underneath. @@ -317,47 +195,7 @@ We will see a better way to create a **session** using a `with` block later. Now that we have some hero model instances (some objects in memory) and a **session**, the next step is to add them to the session: -//// tab | Python 3.10+ - -```Python hl_lines="9-11" -# Code above omitted 👆 -{!./docs_src/tutorial/insert/tutorial001_py310.py[ln:21-30]!} - -# More code here later 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="9-11" -# Code above omitted 👆 -{!./docs_src/tutorial/insert/tutorial001.py[ln:23-32]!} - -# More code here later 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/insert/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/insert/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/insert/tutorial001_py310.py ln[21:30] hl[28:30] *} By this point, our heroes are *not* stored in the database yet. @@ -381,47 +219,7 @@ This ensures that the data is saved in a single batch, and that it will all succ Now that we have the heroes in the **session** and that we are ready to save all that to the database, we can **commit** the changes: -//// tab | Python 3.10+ - -```Python hl_lines="13" -# Code above omitted 👆 -{!./docs_src/tutorial/insert/tutorial001_py310.py[ln:21-32]!} - -# More code here later 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="13" -# Code above omitted 👆 -{!./docs_src/tutorial/insert/tutorial001.py[ln:23-34]!} - -# More code here later 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/insert/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/insert/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/insert/tutorial001_py310.py ln[21:32] hl[32] *} Once this line is executed, the **session** will use the **engine** to save all the data in the database by sending the corresponding SQL. @@ -448,87 +246,11 @@ if __name__ == "__main__": But to keep things a bit more organized, let's instead create a new function `main()` that will contain all the code that should be executed when called as an independent script, and we can put there the previous function `create_db_and_tables()`, and add the new function `create_heroes()`: -//// tab | Python 3.10+ - -```Python hl_lines="2 4" -# Code above omitted 👆 -{!./docs_src/tutorial/insert/tutorial002_py310.py[ln:34-36]!} - -# More code here later 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="2 4" -# Code above omitted 👆 -{!./docs_src/tutorial/insert/tutorial002.py[ln:36-38]!} - -# More code here later 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/insert/tutorial002_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/insert/tutorial002.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/insert/tutorial002_py310.py ln[34:36] hl[34,36] *} And then we can call that single `main()` function from that main block: -//// tab | Python 3.10+ - -```Python hl_lines="8" -# Code above omitted 👆 -{!./docs_src/tutorial/insert/tutorial002_py310.py[ln:34-40]!} -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="8" -# Code above omitted 👆 -{!./docs_src/tutorial/insert/tutorial002.py[ln:36-42]!} -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/insert/tutorial002_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/insert/tutorial002.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/insert/tutorial002_py310.py ln[34:40] hl[40] *} By having everything that should happen when called as a script in a single function, we can easily add more code later on. @@ -583,49 +305,7 @@ The **session** holds some resources, like connections from the engine. So once we are done with the session, we should **close** it to make it release those resources and finish its cleanup: -//// tab | Python 3.10+ - -```Python hl_lines="16" -# Code above omitted 👆 - -{!./docs_src/tutorial/insert/tutorial001_py310.py[ln:21-34]!} - -# More code here later 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="16" -# Code above omitted 👆 - -{!./docs_src/tutorial/insert/tutorial001.py[ln:23-36]!} - -# More code here later 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/insert/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/insert/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/insert/tutorial001_py310.py ln[21:34] hl[34] *} But what happens if we forget to close the session? @@ -639,43 +319,7 @@ It's good to know how the `Session` works and how to create and close it manuall But there's a better way to handle the session, using a `with` block: -//// tab | Python 3.10+ - -```Python hl_lines="7-12" -# Code above omitted 👆 -{!./docs_src/tutorial/insert/tutorial002_py310.py[ln:21-31]!} -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="7-12" -# Code above omitted 👆 -{!./docs_src/tutorial/insert/tutorial002.py[ln:23-33]!} -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/insert/tutorial002_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/insert/tutorial002.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/insert/tutorial002_py310.py ln[21:31] hl[26:31] *} This is the same as creating the session manually and then manually closing it. But here, using a `with` block, it will be automatically created when **starting** the `with` block and assigned to the variable `session`, and it will be automatically closed after the `with` block is **finished**. diff --git a/docs/tutorial/limit-and-offset.md b/docs/tutorial/limit-and-offset.md index aa0d659a31..18e8aa14e2 100644 --- a/docs/tutorial/limit-and-offset.md +++ b/docs/tutorial/limit-and-offset.md @@ -14,97 +14,13 @@ We will continue with the same code as before, but we'll modify it a little the Again, we will create several heroes to have some data to select from: -//// tab | Python 3.10+ - -```Python hl_lines="4-10" -# Code above omitted 👆 - -{!./docs_src/tutorial/offset_and_limit/tutorial001_py310.py[ln:21-39]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="4-10" -# Code above omitted 👆 - -{!./docs_src/tutorial/offset_and_limit/tutorial001.py[ln:23-41]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/offset_and_limit/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/offset_and_limit/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/offset_and_limit/tutorial001_py310.py ln[21:39] hl[22:28] *} ## Review Select All This is the code we had to select all the heroes in the `select()` examples: -//// tab | Python 3.10+ - -```Python hl_lines="3-8" -# Code above omitted 👆 - -{!./docs_src/tutorial/select/tutorial003_py310.py[ln:34-39]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="3-8" -# Code above omitted 👆 - -{!./docs_src/tutorial/select/tutorial003.py[ln:36-41]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/select/tutorial003_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/select/tutorial003.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/select/tutorial003_py310.py ln[34:39] hl[34:39] *} But this would get us **all** the heroes at the same time, in a database that could have thousands, that could be problematic. @@ -112,49 +28,7 @@ But this would get us **all** the heroes at the same time, in a database that co We currently have 7 heroes in the database. But we could as well have thousands, so let's limit the results to get only the first 3: -//// tab | Python 3.10+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/offset_and_limit/tutorial001_py310.py[ln:42-47]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/offset_and_limit/tutorial001.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/offset_and_limit/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/offset_and_limit/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/offset_and_limit/tutorial001_py310.py ln[42:47] hl[44] *} The special **select** object we get from `select()` also has a method `.limit()` that we can use to limit the results to a certain number. @@ -217,49 +91,7 @@ How do we get the next 3? We can use `.offset()`: -//// tab | Python 3.10+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/offset_and_limit/tutorial002_py310.py[ln:42-47]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/offset_and_limit/tutorial002.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/offset_and_limit/tutorial002_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/offset_and_limit/tutorial002.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/offset_and_limit/tutorial002_py310.py ln[42:47] hl[44] *} The way this works is that the special **select** object we get from `select()` has methods like `.where()`, `.offset()` and `.limit()`. @@ -298,49 +130,7 @@ INFO Engine [no key 0.00020s] (3, 3) Then to get the next batch of 3 rows we would offset all the ones we already saw, the first 6: -//// tab | Python 3.10+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/offset_and_limit/tutorial003_py310.py[ln:42-47]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/offset_and_limit/tutorial003.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/offset_and_limit/tutorial003_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/offset_and_limit/tutorial003.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/offset_and_limit/tutorial003_py310.py ln[42:47] hl[44] *} The database right now has **only 7 rows**, so this query can only get 1 row. @@ -395,49 +185,7 @@ If you try that in **DB Browser for SQLite**, you will get the same result: Of course, you can also combine `.limit()` and `.offset()` with `.where()` and other methods you will learn about later: -//// tab | Python 3.10+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/offset_and_limit/tutorial004_py310.py[ln:42-47]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/offset_and_limit/tutorial004.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/offset_and_limit/tutorial004_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/offset_and_limit/tutorial004.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/offset_and_limit/tutorial004_py310.py ln[42:47] hl[44] *} ## Run the Program with Limit, Offset, and Where on the Command Line diff --git a/docs/tutorial/many-to-many/create-data.md b/docs/tutorial/many-to-many/create-data.md index bcd8762997..d53a64c5b2 100644 --- a/docs/tutorial/many-to-many/create-data.md +++ b/docs/tutorial/many-to-many/create-data.md @@ -8,101 +8,13 @@ We'll create data for this same **many-to-many** relationship with a link table: We'll continue from where we left off with the previous code. -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/many_to_many/tutorial001_py310.py ln[0] *} ## Create Heroes As we have done before, we'll create a function `create_heroes()` and we'll create some teams and heroes in it: -//// tab | Python 3.10+ - -```Python hl_lines="11" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial001_py310.py[ln:36-54]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="11" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial001_py39.py[ln:42-60]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="11" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial001.py[ln:42-60]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/many_to_many/tutorial001_py310.py ln[36:54] hl[44] *} This is very similar to what we have done before. @@ -116,137 +28,13 @@ See how **Deadpond** now belongs to the two teams? Now let's do as we have done before, `commit` the **session**, `refresh` the data, and print it: -//// tab | Python 3.10+ - -```Python hl_lines="22-25 27-29 31-36" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial001_py310.py[ln:36-69]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="22-25 27-29 31-36" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial001_py39.py[ln:42-75]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="22-25 27-29 31-36" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial001.py[ln:42-75]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/many_to_many/tutorial001_py310.py ln[36:69] hl[55:58,60:62,64:69] *} ## Add to Main As before, add the `create_heroes()` function to the `main()` function to make sure it is called when running this program from the command line: -//// tab | Python 3.10+ - -```Python -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial001_py310.py[ln:72-74]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial001_py39.py[ln:78-80]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial001.py[ln:78-80]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/many_to_many/tutorial001_py310.py ln[72:74] *} ## Run the Program diff --git a/docs/tutorial/many-to-many/create-models-with-link.md b/docs/tutorial/many-to-many/create-models-with-link.md index 308c678c34..69d0f96808 100644 --- a/docs/tutorial/many-to-many/create-models-with-link.md +++ b/docs/tutorial/many-to-many/create-models-with-link.md @@ -12,63 +12,7 @@ As we want to support a **many-to-many** relationship, now we need a **link tabl We can create it just as any other **SQLModel**: -//// tab | Python 3.10+ - -```Python hl_lines="4-6" -{!./docs_src/tutorial/many_to_many/tutorial001_py310.py[ln:1-6]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="6-12" -{!./docs_src/tutorial/many_to_many/tutorial001_py39.py[ln:1-12]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="6-12" -{!./docs_src/tutorial/many_to_many/tutorial001.py[ln:1-12]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/many_to_many/tutorial001_py310.py ln[1:6] hl[4:6] *} This is a **SQLModel** class model table like any other. @@ -82,69 +26,7 @@ And **both fields are primary keys**. We hadn't used this before. 🤓 Let's see the `Team` model, it's almost identical as before, but with a little change: -//// tab | Python 3.10+ - -```Python hl_lines="8" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial001_py310.py[ln:9-14]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="8" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial001_py39.py[ln:15-20]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="8" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial001.py[ln:15-20]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/many_to_many/tutorial001_py310.py ln[9:14] hl[14] *} The **relationship attribute `heroes`** is still a list of heroes, annotated as `List["Hero"]`. Again, we use `"Hero"` in quotes because we haven't declared that class yet by this point in the code (but as you know, editors and **SQLModel** understand that). @@ -158,69 +40,7 @@ And here's the important part to allow the **many-to-many** relationship, we use Let's see the other side, here's the `Hero` model: -//// tab | Python 3.10+ - -```Python hl_lines="9" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial001_py310.py[ln:17-23]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="9" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial001_py39.py[ln:23-29]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="9" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial001.py[ln:23-29]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/many_to_many/tutorial001_py310.py ln[17:23] hl[23] *} We **removed** the previous `team_id` field (column) because now the relationship is done via the link table. 🔥 @@ -238,140 +58,11 @@ And now we have a **`link_model=HeroTeamLink`**. ✨ The same as before, we will have the rest of the code to create the **engine**, and a function to create all the tables `create_db_and_tables()`. -//// tab | Python 3.10+ - -```Python hl_lines="9" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial001_py310.py[ln:26-33]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="9" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial001_py39.py[ln:32-39]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="9" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial001.py[ln:32-39]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001.py!} -``` - -//// - -/// - +{* ./docs_src/tutorial/many_to_many/tutorial001_py310.py ln[26:33] hl[32] *} And as in previous examples, we will add that function to a function `main()`, and we will call that `main()` function in the main block: -//// tab | Python 3.10+ - -```Python hl_lines="4" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial001_py310.py[ln:72-73]!} - # We will do more stuff here later 👈 - -{!./docs_src/tutorial/many_to_many/tutorial001_py310.py[ln:77-78]!} -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="4" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial001_py39.py[ln:78-79]!} - # We will do more stuff here later 👈 - -{!./docs_src/tutorial/many_to_many/tutorial001_py39.py[ln:83-84]!} -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="4" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial001.py[ln:78-79]!} - # We will do more stuff here later 👈 - -{!./docs_src/tutorial/many_to_many/tutorial001.py[ln:83-84]!} -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001.py!} -``` - -//// - -/// - +{* ./docs_src/tutorial/many_to_many/tutorial001_py310.py ln[72:73,77:78] hl[73] *} ## Run the Code diff --git a/docs/tutorial/many-to-many/link-with-extra-fields.md b/docs/tutorial/many-to-many/link-with-extra-fields.md index 653ef223de..7c7756bcd2 100644 --- a/docs/tutorial/many-to-many/link-with-extra-fields.md +++ b/docs/tutorial/many-to-many/link-with-extra-fields.md @@ -32,69 +32,7 @@ We will add a new field `is_training`. And we will also add two **relationship attributes**, for the linked `team` and `hero`: -//// tab | Python 3.10+ - -```Python hl_lines="6 8-9" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial003_py310.py[ln:4-10]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="10 12-13" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial003_py39.py[ln:6-16]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="10 12-13" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial003.py[ln:6-16]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial003_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial003_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial003.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/many_to_many/tutorial003_py310.py ln[4:10] hl[7,9:10] *} The new **relationship attributes** have their own `back_populates` pointing to new relationship attributes we will create in the `Hero` and `Team` models: @@ -115,69 +53,7 @@ Now let's update the `Team` model. We no longer have the `heroes` relationship attribute, and instead we have the new `hero_links` attribute: -//// tab | Python 3.10+ - -```Python hl_lines="8" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial003_py310.py[ln:13-18]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="8" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial003_py39.py[ln:19-24]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="8" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial003.py[ln:19-24]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial003_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial003_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial003.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/many_to_many/tutorial003_py310.py ln[13:18] hl[18] *} ## Update Hero Model @@ -185,69 +61,7 @@ The same with the `Hero` model. We change the `teams` relationship attribute for `team_links`: -//// tab | Python 3.10+ - -```Python hl_lines="9" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial003_py310.py[ln:21-27]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="9" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial003_py39.py[ln:27-33]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="9" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial003.py[ln:27-33]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial003_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial003_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial003.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/many_to_many/tutorial003_py310.py ln[21:27] hl[27] *} ## Create Relationships @@ -255,69 +69,7 @@ Now the process to create relationships is very similar. But now we create the **explicit link models** manually, pointing to their hero and team instances, and specifying the additional link data (`is_training`): -//// tab | Python 3.10+ - -```Python hl_lines="21-30 32-35" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial003_py310.py[ln:40-79]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="21-30 32-35" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial003_py39.py[ln:46-85]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="21-30 32-35" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial003.py[ln:46-85]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial003_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial003_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial003.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/many_to_many/tutorial003_py310.py ln[40:79] hl[58:67,69:72] *} We are just adding the link model instances to the session, because the link model instances are connected to the heroes and teams, they will be also automatically included in the session when we commit. @@ -415,69 +167,7 @@ Now, to add a new relationship, we have to create a new `HeroTeamLink` instance Here we do that in the `update_heroes()` function: -//// tab | Python 3.10+ - -```Python hl_lines="10-15" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial003_py310.py[ln:82-97]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="10-15" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial003_py39.py[ln:88-103]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="10-15" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial003.py[ln:88-103]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial003_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial003_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial003.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/many_to_many/tutorial003_py310.py ln[82:97] hl[89:94] *} ## Run the Program with the New Relationship @@ -558,81 +248,7 @@ So now we want to update the status of `is_training` to `False`. We can do that by iterating on the links: -//// tab | Python 3.10+ - -```Python hl_lines="8-10" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial003_py310.py[ln:82-83]!} - - # Code here omitted 👈 - -{!./docs_src/tutorial/many_to_many/tutorial003_py310.py[ln:99-107]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="8-10" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial003_py39.py[ln:88-89]!} - - # Code here omitted 👈 - -{!./docs_src/tutorial/many_to_many/tutorial003_py39.py[ln:105-113]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="8-10" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial003.py[ln:88-89]!} - - # Code here omitted 👈 - -{!./docs_src/tutorial/many_to_many/tutorial003.py[ln:105-113]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial003_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial003_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial003.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/many_to_many/tutorial003_py310.py ln[82:83,99:107] hl[99:101] *} ## Run the Program with the Updated Relationships diff --git a/docs/tutorial/many-to-many/update-remove-relationships.md b/docs/tutorial/many-to-many/update-remove-relationships.md index 555289a0e7..ebc9ba3a85 100644 --- a/docs/tutorial/many-to-many/update-remove-relationships.md +++ b/docs/tutorial/many-to-many/update-remove-relationships.md @@ -4,33 +4,7 @@ Now we'll see how to update and remove these **many-to-many** relationships. We'll continue from where we left off with the previous code. -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/many_to_many/tutorial001_py310.py ln[0] *} ## Get Data to Update @@ -42,135 +16,11 @@ As you already know how these goes, I'll use the **short version** and get the d And because we are now using `select()`, we also have to import it. -//// tab | Python 3.10+ - -```Python hl_lines="1 5-10" -{!./docs_src/tutorial/many_to_many/tutorial002_py310.py[ln:1]!} - -# Some code here omitted 👈 - -{!./docs_src/tutorial/many_to_many/tutorial002_py310.py[ln:72-77]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="3 7-12" -{!./docs_src/tutorial/many_to_many/tutorial002_py39.py[ln:1-3]!} - -# Some code here omitted 👈 - -{!./docs_src/tutorial/many_to_many/tutorial002_py39.py[ln:78-83]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="3 7-12" -{!./docs_src/tutorial/many_to_many/tutorial002.py[ln:1-3]!} - -# Some code here omitted 👈 - -{!./docs_src/tutorial/many_to_many/tutorial002.py[ln:78-83]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial002_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial002_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial002.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/many_to_many/tutorial002_py310.py ln[1,72:77] hl[1,72:77] *} And of course, we have to add `update_heroes()` to our `main()` function: -//// tab | Python 3.10+ - -```Python hl_lines="6" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial002_py310.py[ln:94-101]!} -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="6" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial002_py39.py[ln:100-107]!} -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="6" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial002.py[ln:100-107]!} -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial002_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial002_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial002.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/many_to_many/tutorial002_py310.py ln[94:101] hl[97] *} ## Add Many-to-Many Relationships @@ -178,69 +28,7 @@ Now let's imagine that **Spider-Boy** thinks that the **Z-Force** team is super We can use the same **relationship attributes** to include `hero_spider_boy` in the `team_z_force.heroes`. -//// tab | Python 3.10+ - -```Python hl_lines="10-12 14-15" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial002_py310.py[ln:72-84]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="10-12 14-15" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial002_py39.py[ln:78-90]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="10-12 14-15" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial002.py[ln:78-90]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial002_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial002_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial002.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/many_to_many/tutorial002_py310.py ln[72:84] hl[79:81,83:84] *} /// tip @@ -325,69 +113,7 @@ Because `hero_spider_boy.teams` is just a list (a special list managed by SQLAlc In this case, we use the method `.remove()`, that takes an item and removes it from the list. -//// tab | Python 3.10+ - -```Python hl_lines="17-19 21-22" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial002_py310.py[ln:72-91]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="17-19 21-22" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial002_py39.py[ln:78-97]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="17-19 21-22" -# Code above omitted 👆 - -{!./docs_src/tutorial/many_to_many/tutorial002.py[ln:78-97]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial002_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial002_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/many_to_many/tutorial002.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/many_to_many/tutorial002_py310.py ln[72:91] hl[86:88,90:91] *} And this time, just to show again that by using `back_populates` **SQLModel** (actually SQLAlchemy) takes care of connecting the models by their relationships, even though we performed the operation from the `hero_spider_boy` object (modifying `hero_spider_boy.teams`), we are adding `team_z_force` to the **session**. And we commit that, without even add `hero_spider_boy`. diff --git a/docs/tutorial/one.md b/docs/tutorial/one.md index 4e770dbc2e..54cdbbea93 100644 --- a/docs/tutorial/one.md +++ b/docs/tutorial/one.md @@ -14,25 +14,7 @@ Let's see the utilities to read a single row. We'll continue with the same examples we have been using in the previous chapters to create and select data and we'll keep updating them. -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/indexes/tutorial002_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/indexes/tutorial002.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/indexes/tutorial002_py310.py ln[0] *} If you already executed the previous examples and have a database with data, **remove the database file** before running each example, that way you won't have duplicate data and you will be able to get the same results. @@ -40,97 +22,13 @@ If you already executed the previous examples and have a database with data, **r We have been iterating over the rows in a `result` object like: -//// tab | Python 3.10+ - -```Python hl_lines="7-8" -# Code above omitted 👆 - -{!./docs_src/tutorial/indexes/tutorial002_py310.py[ln:42-47]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="7-8" -# Code above omitted 👆 - -{!./docs_src/tutorial/indexes/tutorial002.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/indexes/tutorial002_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/indexes/tutorial002.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/indexes/tutorial002_py310.py ln[42:47] hl[46:47] *} But let's say that we are not interested in all the rows, just the **first** one. We can call the `.first()` method on the `results` object to get the first row: -//// tab | Python 3.10+ - -```Python hl_lines="7" -# Code above omitted 👆 - -{!./docs_src/tutorial/one/tutorial001_py310.py[ln:42-47]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="7" -# Code above omitted 👆 - -{!./docs_src/tutorial/one/tutorial001.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/one/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/one/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/one/tutorial001_py310.py ln[42:47] hl[46] *} This will return the first object in the `results` (if there was any). @@ -171,49 +69,7 @@ It would be possible that the SQL query doesn't find any row. In that case, `.first()` will return `None`: -//// tab | Python 3.10+ - -```Python hl_lines="5 7" -# Code above omitted 👆 - -{!./docs_src/tutorial/one/tutorial002_py310.py[ln:42-47]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="5 7" -# Code above omitted 👆 - -{!./docs_src/tutorial/one/tutorial002.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/one/tutorial002_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/one/tutorial002.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/one/tutorial002_py310.py ln[42:47] hl[44,46] *} In this case, as there's no hero with an age less than 25, `.first()` will return `None`. @@ -246,49 +102,7 @@ And if there was more than one, it would mean that there's an error in the syste In that case, instead of `.first()` we can use `.one()`: -//// tab | Python 3.10+ - -```Python hl_lines="7" -# Code above omitted 👆 - -{!./docs_src/tutorial/one/tutorial003_py310.py[ln:42-47]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="7" -# Code above omitted 👆 - -{!./docs_src/tutorial/one/tutorial003.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/one/tutorial003_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/one/tutorial003.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/one/tutorial003_py310.py ln[42:47] hl[46] *} Here we know that there's only one `"Deadpond"`, and there shouldn't be any more than one. @@ -344,49 +158,7 @@ sqlalchemy.exc.MultipleResultsFound: Multiple rows were found when exactly one w Of course, even if we don't duplicate the data, we could get the same error if we send a query that finds more than one row and expect exactly one with `.one()`: -//// tab | Python 3.10+ - -```Python hl_lines="5 7" -# Code above omitted 👆 - -{!./docs_src/tutorial/one/tutorial004_py310.py[ln:42-47]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="5 7" -# Code above omitted 👆 - -{!./docs_src/tutorial/one/tutorial004.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/one/tutorial004_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/one/tutorial004.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/one/tutorial004_py310.py ln[42:47] hl[44,46] *} That would find 2 rows, and would end up with the same error. @@ -394,49 +166,7 @@ That would find 2 rows, and would end up with the same error. And also, if we get no rows at all with `.one()`, it will also raise an error: -//// tab | Python 3.10+ - -```Python hl_lines="5 7" -# Code above omitted 👆 - -{!./docs_src/tutorial/one/tutorial005_py310.py[ln:42-47]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="5 7" -# Code above omitted 👆 - -{!./docs_src/tutorial/one/tutorial005.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/one/tutorial005_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/one/tutorial005.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/one/tutorial005_py310.py ln[42:47] hl[44,46] *} In this case, as there are no heroes with an age less than 25, `.one()` will raise an error. @@ -469,49 +199,7 @@ sqlalchemy.exc.NoResultFound: No row was found when one was required Of course, with `.first()` and `.one()` you would also probably write all that in a more compact form most of the time, all in a single line (or at least a single Python statement): -//// tab | Python 3.10+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/one/tutorial006_py310.py[ln:42-45]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/one/tutorial006.py[ln:44-47]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/one/tutorial006_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/one/tutorial006.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/one/tutorial006_py310.py ln[42:45] hl[44] *} That would result in the same as some examples above. @@ -521,49 +209,7 @@ In many cases you might want to select a single row by its Id column with the ** You could do it the same way we have been doing with a `.where()` and then getting the first item with `.first()`: -//// tab | Python 3.10+ - -```Python hl_lines="5 7" -# Code above omitted 👆 - -{!./docs_src/tutorial/one/tutorial007_py310.py[ln:42-47]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="5 7" -# Code above omitted 👆 - -{!./docs_src/tutorial/one/tutorial007.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/one/tutorial007_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/one/tutorial007.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/one/tutorial007_py310.py ln[42:47] hl[44,46] *} That would work correctly, as expected. But there's a shorter version. 👇 @@ -571,49 +217,7 @@ That would work correctly, as expected. But there's a shorter version. 👇 As selecting a single row by its Id column with the **primary key** is a common operation, there's a shortcut for it: -//// tab | Python 3.10+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/one/tutorial008_py310.py[ln:42-45]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/one/tutorial008.py[ln:44-47]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/one/tutorial008_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/one/tutorial008.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/one/tutorial008_py310.py ln[42:45] hl[44] *} `session.get(Hero, 1)` is an equivalent to creating a `select()`, then filtering by Id using `.where()`, and then getting the first item with `.first()`. @@ -642,49 +246,7 @@ Hero: secret_name='Dive Wilson' age=None id=1 name='Deadpond' `.get()` behaves similar to `.first()`, if there's no data it will simply return `None` (instead of raising an error): -//// tab | Python 3.10+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/one/tutorial009_py310.py[ln:42-45]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/one/tutorial009.py[ln:44-47]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/one/tutorial009_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/one/tutorial009.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/one/tutorial009_py310.py ln[42:45] hl[44] *} Running that will output: diff --git a/docs/tutorial/relationship-attributes/back-populates.md b/docs/tutorial/relationship-attributes/back-populates.md index 0eaa7bb650..7cb9de6c4a 100644 --- a/docs/tutorial/relationship-attributes/back-populates.md +++ b/docs/tutorial/relationship-attributes/back-populates.md @@ -20,63 +20,7 @@ Let's understand that better with an example. Let's see how that works by writing an **incomplete** version first, without `back_populates`: -//// tab | Python 3.10+ - -```Python hl_lines="9 19" -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py[ln:1-19]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="11 21" -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py[ln:1-21]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="11 21" -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py[ln:1-21]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py ln[1:19] hl[9,19] *} ## Read Data Objects @@ -84,69 +28,7 @@ Now, we will get the **Spider-Boy** hero and, *independently*, the **Preventers* As you already know how this works, I won't separate that in a select `statement`, `results`, etc. Let's use the shorter form in a single call: -//// tab | Python 3.10+ - -```Python hl_lines="5-7 9-11" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py[ln:103-111]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="5-7 9-11" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py[ln:105-113]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="5-7 9-11" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py[ln:105-113]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py ln[103:111] hl[105:107,109:111] *} /// tip @@ -158,69 +40,7 @@ When writing your own code, this is probably the style you will use most often, Now, let's print the current **Spider-Boy**, the current **Preventers** team, and particularly, the current **Preventers** list of heroes: -//// tab | Python 3.10+ - -```Python hl_lines="13-15" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py[ln:103-115]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="13-15" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py[ln:105-117]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="13-15" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py[ln:105-117]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py ln[103:115] hl[113:115] *} Up to this point, it's all good. 😊 @@ -242,81 +62,7 @@ Notice that we have **Spider-Boy** there. Now let's update **Spider-Boy**, removing him from the team by setting `hero_spider_boy.team = None` and then let's print this object again: -//// tab | Python 3.10+ - -```Python hl_lines="8 12" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py[ln:103-104]!} - - # Code here omitted 👈 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py[ln:117-121]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="8 12" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py[ln:105-106]!} - - # Code here omitted 👈 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py[ln:119-123]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="8 12" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py[ln:105-106]!} - - # Code here omitted 👈 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py[ln:119-123]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py ln[103:104,117:121] hl[117,121] *} The first important thing is, we *haven't committed* the hero yet, so accessing the list of heroes would not trigger an automatic refresh. @@ -356,81 +102,7 @@ Oh, no! 😱 **Spider-Boy** is still listed there! Now, if we commit it and print again: -//// tab | Python 3.10+ - -```Python hl_lines="8-9 15" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py[ln:103-104]!} - - # Code here omitted 👈 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py[ln:123-130]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="8-9 15" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py[ln:105-106]!} - - # Code here omitted 👈 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py[ln:125-132]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="8-9 15" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py[ln:105-106]!} - - # Code here omitted 👈 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py[ln:125-132]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/back_populates/tutorial001_py310.py ln[103:104,123:130] hl[123:124,130] *} When we access `preventers_team.heroes` after the `commit`, that triggers a refresh, so we get the latest list, without **Spider-Boy**, so that's fine again: @@ -462,141 +134,11 @@ That's what `back_populates` is for. ✨ Let's add it back: -//// tab | Python 3.10+ - -```Python hl_lines="9 19" -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py[ln:1-19]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="11 21" -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py39.py[ln:1-21]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="11 21" -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py[ln:1-21]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py ln[1:19] hl[9,19] *} And we can keep the rest of the code the same: -//// tab | Python 3.10+ - -```Python hl_lines="8 12" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py[ln:103-104]!} - - # Code here omitted 👈 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py[ln:117-121]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="8 12" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py39.py[ln:105-106]!} - - # Code here omitted 👈 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py39.py[ln:119-123]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="8 12" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py[ln:105-106]!} - - # Code here omitted 👈 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py[ln:119-123]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py ln[103:104,117:121] hl[117,121] *} /// tip @@ -629,63 +171,7 @@ Now that you know why `back_populates` is there, let's review the exact value ag It's quite simple code, it's just a string, but it might be confusing to think exactly *what* string should go there: -//// tab | Python 3.10+ - -```Python hl_lines="9 19" -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py[ln:1-19]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="11 21" -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py39.py[ln:1-21]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="11 21" -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py[ln:1-21]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py ln[1:19] hl[9,19] *} The string in `back_populates` is the name of the attribute *in the other* model, that will reference *the current* model. @@ -693,69 +179,7 @@ The string in `back_populates` is the name of the attribute *in the other* model So, in the class `Team`, we have an attribute `heroes` and we declare it with `Relationship(back_populates="team")`. -//// tab | Python 3.10+ - -```Python hl_lines="8" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py[ln:4-9]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="8" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py39.py[ln:6-11]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="8" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py[ln:6-11]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py ln[4:9] hl[9] *} The string in `back_populates="team"` refers to the attribute `team` in the class `Hero` (the other class). @@ -763,69 +187,7 @@ And, in the class `Hero`, we declare an attribute `team`, and we declare it with So, the string `"heroes"` refers to the attribute `heroes` in the class `Team`. -//// tab | Python 3.10+ - -```Python hl_lines="10" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py[ln:12-19]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="10" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py39.py[ln:14-21]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="10" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py[ln:14-21]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/back_populates/tutorial002_py310.py ln[12:19] hl[19] *} /// tip @@ -850,66 +212,4 @@ So, `back_populates` would most probably be something like `"hero"` or `"heroes" -//// tab | Python 3.10+ - -```Python hl_lines="3 10 13 15" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial003_py310.py[ln:27-39]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="3 10 13 15" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial003_py39.py[ln:29-41]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="3 10 13 15" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py[ln:29-41]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial003_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial003_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/back_populates/tutorial003_py310.py ln[27:39] hl[27,34,37,39] *} diff --git a/docs/tutorial/relationship-attributes/cascade-delete-relationships.md b/docs/tutorial/relationship-attributes/cascade-delete-relationships.md index 1604b9780b..1683c9cb18 100644 --- a/docs/tutorial/relationship-attributes/cascade-delete-relationships.md +++ b/docs/tutorial/relationship-attributes/cascade-delete-relationships.md @@ -109,63 +109,7 @@ When creating a `Relationship()`, we can set `cascade_delete=True`. This configures SQLModel to **automatically delete** the related records (heroes) **when the initial one is deleted** (a team). -//// tab | Python 3.10+ - -```Python hl_lines="9" -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py310.py[ln:1-9]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="11" -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py39.py[ln:1-11]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="11" -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001.py[ln:1-11]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py310.py ln[1:9] hl[9] *} With this configuration, when we delete a team, SQLModel (actually SQLAlchemy) will: @@ -211,63 +155,7 @@ The `ondelete` parameter will set a SQL `ON DELETE` in the **foreign key column* If we want to configure the database to **automatically delete** the related records when the parent is deleted, we can set `ondelete="CASCADE"`. -//// tab | Python 3.10+ - -```Python hl_lines="18" -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py310.py[ln:1-19]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="21" -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py39.py[ln:1-23]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="21" -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001.py[ln:1-23]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py310.py ln[1:19] hl[18] *} Now, when we **create the tables** in the database, the `team_id` column in the `Hero` table will have an `ON DELETE CASCADE` in its definition at the database level. @@ -331,69 +219,7 @@ class Team(SQLModel, table=True): Now, when we **delete a team**, we don't need to do anything else, it's **automatically** going to **delete its heroes**. -//// tab | Python 3.10+ - -```Python hl_lines="7" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py310.py[ln:76-82]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="7" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py39.py[ln:80-86]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="7" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001.py[ln:80-86]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py310.py ln[76:82] hl[80] *} ## Confirm Heroes are Deleted @@ -401,69 +227,7 @@ We can confirm that **after deleting the team** `Wakaland`, the heroes `Black Li If we try to select them from the database, we will **no longer find them**. -//// tab | Python 3.10+ - -```Python hl_lines="5 8 10 13" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py310.py[ln:85-95]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="5 8 10 13" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py39.py[ln:89-99]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="5 8 10 13" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001.py[ln:89-99]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001_py310.py ln[85:95] hl[87,90,92,95] *} ## Run the Program with `cascade_delete=True` and `ondelete="CASCADE"` @@ -533,64 +297,7 @@ We can configure the database to **set the foreign key** (the `team_id` in the ` In this case, the side with `Relationship()` won't have `cascade_delete`, but the side with `Field()` and a `foreign_key` will have `ondelete="SET NULL"`. -//// tab | Python 3.10+ - -```Python hl_lines="19" -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py310.py[ln:1-21]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="21" -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py39.py[ln:1-23]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="21" -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002.py[ln:1-23]!} - -# Code below omitted 👇 -``` - - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py310.py ln[1:21] hl[19] *} The configuration above is setting the `team_id` column from the `Hero` table to have an `ON DELETE SET NULL`. @@ -626,69 +333,7 @@ But if you delete a team from code, by default, SQLModel (actually SQLAlchemy) w Removing a team has the **same code** as before, the only thing that changes is the configuration underneath in the database. -//// tab | Python 3.10+ - -```Python hl_lines="7" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py310.py[ln:78-84]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="7" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py39.py[ln:80-86]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="7" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002.py[ln:80-86]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002_py310.py ln[78:84] hl[82] *} The result would be these tables. @@ -793,69 +438,7 @@ If you know your database would be able to correctly handle the deletes or updat To be able to test this out with SQLite, we first need to enable foreign key support. -//// tab | Python 3.10+ - -```Python hl_lines="6" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py310.py[ln:30-33]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="6" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py39.py[ln:32-35]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="6" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003.py[ln:32-35]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py310.py ln[30:33] hl[33] *} /// info @@ -869,63 +452,7 @@ Now let's update the table model for `Team` to use `passive_deletes="all"` in th We will also use `ondelete="SET NULL"` in the `Hero` model table, in the foreign key `Field()` for the `team_id` to make the database set those fields to `NULL` automatically. -//// tab | Python 3.10+ - -```Python hl_lines="9 19" -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py310.py[ln:1-21]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="11 21" -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py39.py[ln:1-23]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="11 21" -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003.py[ln:1-23]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003_py310.py ln[1:21] hl[9,19] *} ### Run the Program with `passive_deletes` @@ -989,69 +516,7 @@ For SQLite, this also needs enabling foreign key support. As `ondelete="RESTRICT"` is mainly a database-level constraint, let's enable foreign key support in SQLite first to be able to test it. -//// tab | Python 3.10+ - -```Python hl_lines="6" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py310.py[ln:30-33]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="6" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py39.py[ln:32-35]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="6" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004.py[ln:32-35]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py310.py ln[30:33] hl[33] *} ### Use `ondelete="RESTRICT"` @@ -1065,63 +530,7 @@ Notice that we don't set `cascade_delete` in the `Team` model table. /// -//// tab | Python 3.10+ - -```Python hl_lines="9 19" -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py310.py[ln:1-21]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="11 21" -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py39.py[ln:1-23]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="11 21" -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004.py[ln:1-23]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004_py310.py ln[1:21] hl[9,19] *} ### Run the Program with `RESTRICT`, See the Error @@ -1184,69 +593,7 @@ Calling `team.heroes.clear()` is very similar to what SQLModel (actually SQLAlch /// -//// tab | Python 3.10+ - -```Python hl_lines="7" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005_py310.py[ln:80-88]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="7" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005_py39.py[ln:82-90]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="7" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005.py[ln:82-90]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005_py310.py ln[80:88] hl[84] *} ### Run the Program Deleting Heroes First diff --git a/docs/tutorial/relationship-attributes/create-and-update-relationships.md b/docs/tutorial/relationship-attributes/create-and-update-relationships.md index ce2f4d87d4..d0de56735d 100644 --- a/docs/tutorial/relationship-attributes/create-and-update-relationships.md +++ b/docs/tutorial/relationship-attributes/create-and-update-relationships.md @@ -6,49 +6,7 @@ Let's see now how to create data with relationships using these new **relationsh Let's check the old code we used to create some heroes and teams: -//// tab | Python 3.10+ - -```Python hl_lines="9 12 18 24" -# Code above omitted 👆 - -{!./docs_src/tutorial/connect/insert/tutorial001_py310.py[ln:29-58]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="9 12 18 24" -# Code above omitted 👆 - -{!./docs_src/tutorial/connect/insert/tutorial001.py[ln:31-60]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/connect/insert/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/connect/insert/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/connect/insert/tutorial001_py310.py ln[29:58] hl[35,38,44,50] *} There are several things to **notice** here. @@ -68,69 +26,7 @@ This is the first area where these **relationship attributes** can help. 🤓 Now let's do all that, but this time using the new, shiny `Relationship` attributes: -//// tab | Python 3.10+ - -```Python hl_lines="9 12 18" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py[ln:32-55]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="9 12 18" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py39.py[ln:34-57]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="9 12 18" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py[ln:34-57]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py ln[32:55] hl[38,41,47] *} Now we can create the `Team` instances and pass them directly to the new `team` argument when creating the `Hero` instances, as `team=team_preventers` instead of `team_id=team_preventers.id`. @@ -146,81 +42,7 @@ And then, as you can see, we only have to do one `commit()`. The same way we could assign an integer with a `team.id` to a `hero.team_id`, we can also assign the `Team` instance to the `hero.team`: -//// tab | Python 3.10+ - -```Python hl_lines="8" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py[ln:32-33]!} - - # Previous code here omitted 👈 - -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py[ln:57-61]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="8" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py39.py[ln:34-35]!} - - # Previous code here omitted 👈 - -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py39.py[ln:59-63]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="8" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py[ln:34-35]!} - - # Previous code here omitted 👈 - -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py[ln:59-63]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py ln[32:33,57:61] hl[57] *} ## Create a Team with Heroes @@ -228,81 +50,7 @@ Before, we created some `Team` instances and passed them in the `team=` argument We could also create the `Hero` instances first, and then pass them in the `heroes=` argument that takes a list, when creating a `Team` instance: -//// tab | Python 3.10+ - -```Python hl_lines="13 15-16" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py[ln:32-33]!} - - # Previous code here omitted 👈 - -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py[ln:63-73]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="13 15-16" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py39.py[ln:34-35]!} - - # Previous code here omitted 👈 - -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py39.py[ln:65-75]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="13 15-16" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py[ln:34-35]!} - - # Previous code here omitted 👈 - -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py[ln:65-75]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py ln[32:33,63:73] hl[68,70:71] *} Here we create two heroes first, **Black Lion** and **Princess Sure-E**, and then we pass them in the `heroes` argument. @@ -318,81 +66,7 @@ As the attribute `team.heroes` behaves like a list, we can simply append to it. Let's create some more heroes and add them to the `team_preventers.heroes` list attribute: -//// tab | Python 3.10+ - -```Python hl_lines="14-18" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py[ln:32-33]!} - - # Previous code here omitted 👈 - -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py[ln:75-91]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="14-18" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py39.py[ln:34-35]!} - - # Previous code here omitted 👈 - -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py39.py[ln:77-93]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="14-18" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py[ln:34-35]!} - - # Previous code here omitted 👈 - -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py[ln:77-93]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001_py310.py ln[32:33,75:91] hl[81:85] *} The attribute `team_preventers.heroes` behaves like a list. But it's a special type of list, because when we modify it adding heroes to it, **SQLModel** (actually SQLAlchemy) **keeps track of the necessary changes** to be done in the database. diff --git a/docs/tutorial/relationship-attributes/define-relationships-attributes.md b/docs/tutorial/relationship-attributes/define-relationships-attributes.md index adbcc7df75..7839891988 100644 --- a/docs/tutorial/relationship-attributes/define-relationships-attributes.md +++ b/docs/tutorial/relationship-attributes/define-relationships-attributes.md @@ -41,45 +41,7 @@ Now that you know how these tables work underneath and how the model classes rep Up to now, we have only used the `team_id` column to connect the tables when querying with `select()`: -//// tab | Python 3.10+ - -```Python hl_lines="16" -{!./docs_src/tutorial/connect/insert/tutorial001_py310.py[ln:1-16]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="18" -{!./docs_src/tutorial/connect/insert/tutorial001.py[ln:1-18]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/connect/insert/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/connect/insert/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/connect/insert/tutorial001_py310.py ln[1:16] hl[16] *} This is a **plain field** like all the others, all representing a **column in the table**. @@ -87,123 +49,11 @@ But now let's add a couple of new special attributes to these model classes, let First, import `Relationship` from `sqlmodel`: -//// tab | Python 3.10+ - -```Python hl_lines="1" -{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py[ln:1]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="3" -{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py39.py[ln:1-3]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="3" -{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py[ln:1-3]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py ln[1] hl[1] *} Next, use that `Relationship` to declare a new attribute in the model classes: -//// tab | Python 3.10+ - -```Python hl_lines="9 19" -{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py[ln:1-19]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="11 21" -{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py39.py[ln:1-21]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="11 21" -{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py[ln:1-21]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py ln[1:19] hl[9,19] *} ## What Are These Relationship Attributes diff --git a/docs/tutorial/relationship-attributes/read-relationships.md b/docs/tutorial/relationship-attributes/read-relationships.md index fe9e5ae02a..a59ec8351e 100644 --- a/docs/tutorial/relationship-attributes/read-relationships.md +++ b/docs/tutorial/relationship-attributes/read-relationships.md @@ -6,81 +6,7 @@ Now that we know how to connect data using **relationship Attributes**, let's se First, add a function `select_heroes()` where we get a hero to start working with, and add that function to the `main()` function: -//// tab | Python 3.10+ - -```Python hl_lines="3-7 14" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py310.py[ln:94-98]!} - -# Previous code here omitted 👈 - -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py310.py[ln:108-111]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="3-7 14" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py39.py[ln:96-100]!} - -# Previous code here omitted 👈 - -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py39.py[ln:110-113]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="3-7 14" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py[ln:96-100]!} - -# Previous code here omitted 👈 - -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py[ln:110-113]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py310.py ln[94:98,108:111] hl[94:98,111] *} ## Select the Related Team - Old Way @@ -88,69 +14,7 @@ Now that we have a hero, we can get the team this hero belongs to. With what we have learned **up to now**, we could use a `select()` statement, then execute it with `session.exec()`, and then get the `.first()` result, for example: -//// tab | Python 3.10+ - -```Python hl_lines="9-12" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py310.py[ln:94-103]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="9-12" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py39.py[ln:96-105]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="9-12" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py[ln:96-105]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py310.py ln[94:103] hl[100:103] *} ## Get Relationship Team - New Way @@ -158,81 +22,7 @@ But now that we have the **relationship attributes**, we can just access them, a So, the highlighted block above, has the same results as the block below: -//// tab | Python 3.10+ - -```Python hl_lines="11" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py310.py[ln:94-98]!} - - # Code from the previous example omitted 👈 - -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py310.py[ln:105]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="11" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py39.py[ln:96-100]!} - - # Code from the previous example omitted 👈 - -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py39.py[ln:107]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="11" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py[ln:96-100]!} - - # Code from the previous example omitted 👈 - -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py[ln:107]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/read_relationships/tutorial001_py310.py ln[94:98,105] hl[105] *} /// tip @@ -246,69 +36,7 @@ For example, here, **inside** a `with` block with a `Session` object. And the same way, when we are working on the **many** side of the **one-to-many** relationship, we can get a list of of the related objects just by accessing the relationship attribute: -//// tab | Python 3.10+ - -```Python hl_lines="9" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py310.py[ln:94-100]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="9" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py39.py[ln:96-102]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="9" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py[ln:96-102]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py310.py ln[94:100] hl[100] *} That would print a list with all the heroes in the Preventers team: diff --git a/docs/tutorial/relationship-attributes/remove-relationships.md b/docs/tutorial/relationship-attributes/remove-relationships.md index 56745850b5..ff408e20b9 100644 --- a/docs/tutorial/relationship-attributes/remove-relationships.md +++ b/docs/tutorial/relationship-attributes/remove-relationships.md @@ -8,135 +8,11 @@ And then for some reason needs to leave the **Preventers** for some years. 😭 We can remove the relationship by setting it to `None`, the same as with the `team_id`, it also works with the new relationship attribute `.team`: -//// tab | Python 3.10+ - -```Python hl_lines="9" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py310.py[ln:103-114]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="9" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py39.py[ln:105-116]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="9" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py[ln:105-116]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py310.py ln[103:114] hl[109] *} And of course, we should remember to add this `update_heroes()` function to `main()` so that it runs when we call this program from the command line: -//// tab | Python 3.10+ - -```Python hl_lines="7" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py310.py[ln:117-121]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="7" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py39.py[ln:119-123]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="7" -# Code above omitted 👆 - -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py[ln:119-123]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/read_relationships/tutorial002_py310.py ln[117:121] hl[121] *} ## Recap diff --git a/docs/tutorial/relationship-attributes/type-annotation-strings.md b/docs/tutorial/relationship-attributes/type-annotation-strings.md index a798af9aac..026036a2f4 100644 --- a/docs/tutorial/relationship-attributes/type-annotation-strings.md +++ b/docs/tutorial/relationship-attributes/type-annotation-strings.md @@ -2,63 +2,7 @@ In the first Relationship attribute, we declare it with `List["Hero"]`, putting the `Hero` in quotes instead of just normally there: -//// tab | Python 3.10+ - -```Python hl_lines="9" -{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py[ln:1-19]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.9+ - -```Python hl_lines="11" -{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py39.py[ln:1-21]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="11" -{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py[ln:1-21]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.9+ - -```Python -{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py39.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py ln[1:19] hl[9] *} What's that about? Can't we just write it normally as `List[Hero]`? diff --git a/docs/tutorial/select.md b/docs/tutorial/select.md index be66de351e..d130b514fd 100644 --- a/docs/tutorial/select.md +++ b/docs/tutorial/select.md @@ -23,25 +23,7 @@ Things are getting more exciting! Let's now see how to read data from the databa Let's continue from the last code we used to create some data. -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/insert/tutorial002_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/insert/tutorial002.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/insert/tutorial002_py310.py ln[0] *} We are creating a **SQLModel** `Hero` class model and creating some records. @@ -178,49 +160,7 @@ The first step is to create a **Session**, the same way we did when creating the We will start with that in a new function `select_heroes()`: -//// tab | Python 3.10+ - -```Python hl_lines="3-4" -# Code above omitted 👆 - -{!./docs_src/tutorial/select/tutorial001_py310.py[ln:34-35]!} - -# More code here later 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="3-4" -# Code above omitted 👆 - -{!./docs_src/tutorial/select/tutorial001.py[ln:36-37]!} - -# More code here later 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/select/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/select/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/select/tutorial001_py310.py ln[34:35] hl[34:35] *} ## Create a `select` Statement @@ -228,95 +168,11 @@ Next, pretty much the same way we wrote a SQL `SELECT` statement above, now we'l First we have to import `select` from `sqlmodel` at the top of the file: -//// tab | Python 3.10+ - -```Python hl_lines="1" -{!./docs_src/tutorial/select/tutorial001_py310.py[ln:1]!} - -# More code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="3" -{!./docs_src/tutorial/select/tutorial001.py[ln:1-3]!} - -# More code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/select/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/select/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/select/tutorial001_py310.py ln[1] hl[1] *} And then we will use it to create a `SELECT` statement in Python code: -//// tab | Python 3.10+ - -```Python hl_lines="7" -{!./docs_src/tutorial/select/tutorial001_py310.py[ln:1]!} - -# More code here omitted 👈 - -{!./docs_src/tutorial/select/tutorial001_py310.py[ln:34-36]!} - -# More code here later 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="9" -{!./docs_src/tutorial/select/tutorial001.py[ln:1-3]!} - -# More code here omitted 👈 - -{!./docs_src/tutorial/select/tutorial001.py[ln:36-38]!} - -# More code here later 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/select/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/select/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/select/tutorial001_py310.py ln[1,34:36] hl[36] *} It's a very simple line of code that conveys a lot of information: @@ -347,49 +203,7 @@ I'll tell you about that in the next chapters. Now that we have the `select` statement, we can execute it with the **session**: -//// tab | Python 3.10+ - -```Python hl_lines="6" -# Code above omitted 👆 - -{!./docs_src/tutorial/select/tutorial001_py310.py[ln:34-37]!} - -# More code here later 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="6" -# Code above omitted 👆 - -{!./docs_src/tutorial/select/tutorial001.py[ln:36-39]!} - -# More code here later 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/select/tutorial001_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/select/tutorial001.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/select/tutorial001_py310.py ln[34:37] hl[37] *} This will tell the **session** to go ahead and use the **engine** to execute that `SELECT` statement in the database and bring the results back. @@ -427,49 +241,7 @@ The `results` object is an ` to get the rows where a column is **more than** a value: -//// tab | Python 3.10+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/where/tutorial003_py310.py[ln:42-47]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/where/tutorial003.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/where/tutorial003_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/where/tutorial003.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/where/tutorial003_py310.py ln[42:47] hl[44] *} That would output: @@ -795,49 +525,7 @@ Notice that it didn't select `Black Lion`, because the age is not *strictly* gre Let's do that again, but with `>=` to get the rows where a column is **more than or equal** to a value: -//// tab | Python 3.10+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/where/tutorial004_py310.py[ln:42-47]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/where/tutorial004.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/where/tutorial004_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/where/tutorial004.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/where/tutorial004_py310.py ln[42:47] hl[44] *} Because we are using `>=`, the age `35` will be included in the output: @@ -858,49 +546,7 @@ This time we got `Black Lion` too because although the age is not *strictly* gre Similarly, we can use `<` to get the rows where a column is **less than** a value: -//// tab | Python 3.10+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/where/tutorial005_py310.py[ln:42-47]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/where/tutorial005.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/where/tutorial005_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/where/tutorial005.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/where/tutorial005_py310.py ln[42:47] hl[44] *} And we get the younger one with an age in the database: @@ -918,49 +564,7 @@ We could imagine that **Spider-Boy** is even **younger**. But because we don't k Finally, we can use `<=` to get the rows where a column is **less than or equal** to a value: -//// tab | Python 3.10+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/where/tutorial006_py310.py[ln:42-47]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/where/tutorial006.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/where/tutorial006_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/where/tutorial006.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/where/tutorial006_py310.py ln[42:47] hl[44] *} And we get the younger ones, `35` and below: @@ -985,49 +589,7 @@ We can use the same standard Python comparison operators like `<`, `<=`, `>`, `> Because `.where()` returns the same special select object back, we can add more `.where()` calls to it: -//// tab | Python 3.10+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/where/tutorial007_py310.py[ln:42-47]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/where/tutorial007.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/where/tutorial007_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/where/tutorial007.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/where/tutorial007_py310.py ln[42:47] hl[44] *} This will select the rows `WHERE` the `age` is **greater than or equal** to `35`, `AND` also the `age` is **less than** `40`. @@ -1068,49 +630,7 @@ age=36 id=6 name='Dr. Weird' secret_name='Steve Weird' As an alternative to using multiple `.where()` we can also pass several expressions to a single `.where()`: -//// tab | Python 3.10+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/where/tutorial008_py310.py[ln:42-47]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/where/tutorial008.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/where/tutorial008_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/where/tutorial008.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/where/tutorial008_py310.py ln[42:47] hl[44] *} This is the same as the above, and will result in the same output with the two heroes: @@ -1127,93 +647,13 @@ But we can also combine expressions using `OR`. Which means that **any** (but no To do it, you can import `or_`: -//// tab | Python 3.10+ - -```Python hl_lines="1" -{!./docs_src/tutorial/where/tutorial009_py310.py[ln:1]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="3" -{!./docs_src/tutorial/where/tutorial009.py[ln:1-3]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/where/tutorial009_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/where/tutorial009.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/where/tutorial009_py310.py ln[1] hl[1] *} And then pass both expressions to `or_()` and put it inside `.where()`. For example, here we select the heroes that are the youngest OR the oldest: -//// tab | Python 3.10+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/where/tutorial009_py310.py[ln:42-47]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/where/tutorial009.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/where/tutorial009_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/where/tutorial009.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/where/tutorial009_py310.py ln[42:47] hl[44] *} When we run it, this generates the output: @@ -1264,91 +704,11 @@ We can tell the editor that this class attribute is actually a special **SQLMode To do that, we can import `col()` (as short for "column"): -//// tab | Python 3.10+ - -```Python hl_lines="1" -{!./docs_src/tutorial/where/tutorial011_py310.py[ln:1]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="3" -{!./docs_src/tutorial/where/tutorial011.py[ln:1-3]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/where/tutorial011_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/where/tutorial011.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/where/tutorial011_py310.py ln[1] hl[1] *} And then put the **class attribute** inside `col()` when using it in a `.where()`: -//// tab | Python 3.10+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/where/tutorial011_py310.py[ln:42-47]!} - -# Code below omitted 👇 -``` - -//// - -//// tab | Python 3.7+ - -```Python hl_lines="5" -# Code above omitted 👆 - -{!./docs_src/tutorial/where/tutorial011.py[ln:44-49]!} - -# Code below omitted 👇 -``` - -//// - -/// details | 👀 Full file preview - -//// tab | Python 3.10+ - -```Python -{!./docs_src/tutorial/where/tutorial011_py310.py!} -``` - -//// - -//// tab | Python 3.7+ - -```Python -{!./docs_src/tutorial/where/tutorial011.py!} -``` - -//// - -/// +{* ./docs_src/tutorial/where/tutorial011_py310.py ln[42:47] hl[44] *} So, now the comparison is not: diff --git a/requirements-docs.txt b/requirements-docs.txt index 4e8c91466e..31f1a6d02d 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -16,4 +16,4 @@ cairosvg==2.7.1 # For griffe, it formats with black typer == 0.12.3 mkdocs-macros-plugin==1.0.5 -markdown-include-variants==0.0.3 +markdown-include-variants==0.0.4 From 6680b3c26f7119703e1e9fafc618ab89d3c6b6fa Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 22 Dec 2024 14:30:24 +0000 Subject: [PATCH 557/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 6805a0dc31..0501f776db 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Docs +* 📝 Update markdown includes format. PR [#1254](https://github.com/fastapi/sqlmodel/pull/1254) by [@tiangolo](https://github.com/tiangolo). * 📝 Update fenced code in Decimal docs for consistency. PR [#1251](https://github.com/fastapi/sqlmodel/pull/1251) by [@tiangolo](https://github.com/tiangolo). * ✏️ Fix typo in the release notes of v0.0.22. PR [#1195](https://github.com/fastapi/sqlmodel/pull/1195) by [@PipeKnight](https://github.com/PipeKnight). * 📝 Update includes for `docs/advanced/uuid.md`. PR [#1151](https://github.com/fastapi/sqlmodel/pull/1151) by [@tiangolo](https://github.com/tiangolo). From 7dbc1c934acff34fe5cafce4924e7f1ee1d3020f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 22 Dec 2024 14:44:58 +0000 Subject: [PATCH 558/906] =?UTF-8?q?=E2=AC=86=20Bump=20pillow=20from=2010.3?= =?UTF-8?q?.0=20to=2011.0.0=20(#1139)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [pillow](https://github.com/python-pillow/Pillow) from 10.3.0 to 11.0.0. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/10.3.0...11.0.0) --- updated-dependencies: - dependency-name: pillow dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 31f1a6d02d..da15671f50 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -7,7 +7,7 @@ pyyaml >=5.3.1,<7.0.0 # For Material for MkDocs, Chinese search # jieba==0.42.1 # For image processing by Material for MkDocs -pillow==10.3.0 +pillow==11.0.0 # For image processing by Material for MkDocs cairosvg==2.7.1 # mkdocstrings[python]==0.25.1 From 5699c43ee53b2d6f420fbd591e6605f1966c8073 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 22 Dec 2024 14:45:21 +0000 Subject: [PATCH 559/906] =?UTF-8?q?=E2=AC=86=20Bump=20astral-sh/setup-uv?= =?UTF-8?q?=20from=204=20to=205=20(#1249)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 4 to 5. - [Release notes](https://github.com/astral-sh/setup-uv/releases) - [Commits](https://github.com/astral-sh/setup-uv/compare/v4...v5) --- updated-dependencies: - dependency-name: astral-sh/setup-uv dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-docs.yml | 2 +- .github/workflows/deploy-docs.yml | 2 +- .github/workflows/smokeshow.yml | 2 +- .github/workflows/test.yml | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 590cf8532b..827d1518e6 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -59,7 +59,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v4 + uses: astral-sh/setup-uv@v5 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index d8634e253f..01e0346d23 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -29,7 +29,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v4 + uses: astral-sh/setup-uv@v5 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index 7a1c88ddd9..0a1255b8a7 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -21,7 +21,7 @@ jobs: with: python-version: '3.9' - name: Setup uv - uses: astral-sh/setup-uv@v4 + uses: astral-sh/setup-uv@v5 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4590e080c3..b5628651a6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,7 +44,7 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Setup uv - uses: astral-sh/setup-uv@v4 + uses: astral-sh/setup-uv@v5 with: version: "0.4.15" enable-cache: true @@ -92,7 +92,7 @@ jobs: with: python-version: '3.12' - name: Setup uv - uses: astral-sh/setup-uv@v4 + uses: astral-sh/setup-uv@v5 with: version: "0.4.15" enable-cache: true From b6bc7de26390c06e1b2fea845690465bcf68d9ca Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 22 Dec 2024 14:45:26 +0000 Subject: [PATCH 560/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 0501f776db..4097d90029 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* ⬆ Bump pillow from 10.3.0 to 11.0.0. PR [#1139](https://github.com/fastapi/sqlmodel/pull/1139) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pypa/gh-action-pypi-publish from 1.9.0 to 1.12.3. PR [#1240](https://github.com/fastapi/sqlmodel/pull/1240) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump astral-sh/setup-uv from 3 to 4. PR [#1225](https://github.com/fastapi/sqlmodel/pull/1225) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump tiangolo/latest-changes from 0.3.1 to 0.3.2. PR [#1207](https://github.com/fastapi/sqlmodel/pull/1207) by [@dependabot[bot]](https://github.com/apps/dependabot). From cc0e6d6146c1d45acd0594233dc589e181b6665d Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 22 Dec 2024 14:45:40 +0000 Subject: [PATCH 561/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 4097d90029..90511d52a7 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* ⬆ Bump astral-sh/setup-uv from 4 to 5. PR [#1249](https://github.com/fastapi/sqlmodel/pull/1249) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pillow from 10.3.0 to 11.0.0. PR [#1139](https://github.com/fastapi/sqlmodel/pull/1139) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pypa/gh-action-pypi-publish from 1.9.0 to 1.12.3. PR [#1240](https://github.com/fastapi/sqlmodel/pull/1240) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump astral-sh/setup-uv from 3 to 4. PR [#1225](https://github.com/fastapi/sqlmodel/pull/1225) by [@dependabot[bot]](https://github.com/apps/dependabot). From c889139491a5b647d17eb5855739ccae95aaf4e5 Mon Sep 17 00:00:00 2001 From: Noushadali <88229902+Noushadaliam@users.noreply.github.com> Date: Thu, 26 Dec 2024 16:48:55 +0100 Subject: [PATCH 562/906] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo=20in=20?= =?UTF-8?q?`insert.md`=20(#1256)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix: Typo in insert.md Fixed the typo in 'left of' with 'left off'. --- docs/tutorial/insert.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/insert.md b/docs/tutorial/insert.md index 272287e624..2110e4c0b2 100644 --- a/docs/tutorial/insert.md +++ b/docs/tutorial/insert.md @@ -21,7 +21,7 @@ Here's a reminder of how the table would look like, this is the data we want to ## Create Table and Database -We will continue from where we left of in the last chapter. +We will continue from where we left off in the last chapter. This is the code we had to create the database and table, nothing new here: From fa5895c6d106a24cad88ef1923819e37674f987b Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 26 Dec 2024 15:49:14 +0000 Subject: [PATCH 563/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 90511d52a7..855af07e77 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Docs +* ✏️ Fix typo in `insert.md`. PR [#1256](https://github.com/fastapi/sqlmodel/pull/1256) by [@Noushadaliam](https://github.com/Noushadaliam). * 📝 Update markdown includes format. PR [#1254](https://github.com/fastapi/sqlmodel/pull/1254) by [@tiangolo](https://github.com/tiangolo). * 📝 Update fenced code in Decimal docs for consistency. PR [#1251](https://github.com/fastapi/sqlmodel/pull/1251) by [@tiangolo](https://github.com/tiangolo). * ✏️ Fix typo in the release notes of v0.0.22. PR [#1195](https://github.com/fastapi/sqlmodel/pull/1195) by [@PipeKnight](https://github.com/PipeKnight). From 073e4f9fd112749e419e46ced8e3d970f9a96f91 Mon Sep 17 00:00:00 2001 From: ArianHamdi <59548833+ArianHamdi@users.noreply.github.com> Date: Fri, 3 Jan 2025 01:16:19 +0400 Subject: [PATCH 564/906] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo=20in=20?= =?UTF-8?q?`docs/tutorial/create-db-and-table.md`=20(#1252)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tutorial/create-db-and-table.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/create-db-and-table.md b/docs/tutorial/create-db-and-table.md index 978413c334..c1076d5887 100644 --- a/docs/tutorial/create-db-and-table.md +++ b/docs/tutorial/create-db-and-table.md @@ -446,7 +446,7 @@ Now we would be able to, for example, import the `Hero` class in some other file We prevented the side effects when importing something from your `app.py` file. -But we still want it to **create the database and table** when we call it with Python directly as an independent script from the terminal, just as as above. +But we still want it to **create the database and table** when we call it with Python directly as an independent script from the terminal, just as above. /// tip From 7d00768456cdbffdfd0fae28b8608427d674aee1 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 2 Jan 2025 21:16:45 +0000 Subject: [PATCH 565/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 855af07e77..ab936d08bf 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Docs +* ✏️ Fix typo in `docs/tutorial/create-db-and-table.md`. PR [#1252](https://github.com/fastapi/sqlmodel/pull/1252) by [@ArianHamdi](https://github.com/ArianHamdi). * ✏️ Fix typo in `insert.md`. PR [#1256](https://github.com/fastapi/sqlmodel/pull/1256) by [@Noushadaliam](https://github.com/Noushadaliam). * 📝 Update markdown includes format. PR [#1254](https://github.com/fastapi/sqlmodel/pull/1254) by [@tiangolo](https://github.com/tiangolo). * 📝 Update fenced code in Decimal docs for consistency. PR [#1251](https://github.com/fastapi/sqlmodel/pull/1251) by [@tiangolo](https://github.com/tiangolo). From cd05bfeaf2fed3d38fc67b71d07b8edcf4c0a39c Mon Sep 17 00:00:00 2001 From: Sofie Van Landeghem Date: Thu, 20 Feb 2025 10:15:20 +0100 Subject: [PATCH 566/906] =?UTF-8?q?=F0=9F=91=B7=20Add=20retries=20to=20Smo?= =?UTF-8?q?keshow=20(#1302)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/smokeshow.yml | 12 +++++++++++- .github/workflows/test.yml | 1 - 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index 0a1255b8a7..5575d44abb 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -35,7 +35,17 @@ jobs: path: htmlcov github-token: ${{ secrets.GITHUB_TOKEN }} run-id: ${{ github.event.workflow_run.id }} - - run: smokeshow upload htmlcov + # Try 5 times to upload coverage to smokeshow + - name: Upload coverage to Smokeshow + run: | + for i in 1 2 3 4 5; do + if smokeshow upload htmlcov; then + echo "Smokeshow upload success!" + break + fi + echo "Smokeshow upload error, sleep 1 sec and try again." + sleep 1 + done env: SMOKESHOW_GITHUB_STATUS_DESCRIPTION: Coverage {coverage-percentage} SMOKESHOW_GITHUB_COVERAGE_THRESHOLD: 95 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b5628651a6..9126db93c5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,7 +27,6 @@ jobs: strategy: matrix: python-version: - - "3.7" - "3.8" - "3.9" - "3.10" From 10231263f2f914d2a8a595ed53cc3a831e28fa46 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 20 Feb 2025 09:15:48 +0000 Subject: [PATCH 567/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index ab936d08bf..98bbe2446b 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -21,6 +21,7 @@ ### Internal +* 👷 Add retries to Smokeshow. PR [#1302](https://github.com/fastapi/sqlmodel/pull/1302) by [@svlandeg](https://github.com/svlandeg). * ⬆ Bump astral-sh/setup-uv from 4 to 5. PR [#1249](https://github.com/fastapi/sqlmodel/pull/1249) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pillow from 10.3.0 to 11.0.0. PR [#1139](https://github.com/fastapi/sqlmodel/pull/1139) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pypa/gh-action-pypi-publish from 1.9.0 to 1.12.3. PR [#1240](https://github.com/fastapi/sqlmodel/pull/1240) by [@dependabot[bot]](https://github.com/apps/dependabot). From 8daa54443de01a8246c62db3dd1dfff27606700f Mon Sep 17 00:00:00 2001 From: Radi Date: Thu, 20 Feb 2025 16:33:38 +0100 Subject: [PATCH 568/906] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo=20in=20?= =?UTF-8?q?`databases.md`=20(#1113)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update databases.md typo fix Co-authored-by: Sofie Van Landeghem --- docs/databases.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/databases.md b/docs/databases.md index d65a7431f4..0f99f4509d 100644 --- a/docs/databases.md +++ b/docs/databases.md @@ -66,7 +66,7 @@ There are many databases of many types. ### A single file database -A database could be a single file called `heroes.db`, managed with code in a very efficient way. An example would be SQLite, more about that on a bit. +A database could be a single file called `heroes.db`, managed with code in a very efficient way. An example would be SQLite, more about that in a bit. ![database as a single file](img/databases/single-file.svg) From ee16ba4dc327ac343c55ebbc9fa9234385b74b2b Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 20 Feb 2025 15:34:01 +0000 Subject: [PATCH 569/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 98bbe2446b..c341c54add 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Docs +* ✏️ Fix typo in `databases.md`. PR [#1113](https://github.com/fastapi/sqlmodel/pull/1113) by [@radi-dev](https://github.com/radi-dev). * ✏️ Fix typo in `docs/tutorial/create-db-and-table.md`. PR [#1252](https://github.com/fastapi/sqlmodel/pull/1252) by [@ArianHamdi](https://github.com/ArianHamdi). * ✏️ Fix typo in `insert.md`. PR [#1256](https://github.com/fastapi/sqlmodel/pull/1256) by [@Noushadaliam](https://github.com/Noushadaliam). * 📝 Update markdown includes format. PR [#1254](https://github.com/fastapi/sqlmodel/pull/1254) by [@tiangolo](https://github.com/tiangolo). From 759220d5928af4a01c205a039eb9826456a475cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 21 Feb 2025 12:59:48 +0100 Subject: [PATCH 570/906] =?UTF-8?q?=20=F0=9F=91=B7=20Add=20Codecov=20to=20?= =?UTF-8?q?CI,=20Smokeshow/Cloudflare=20has=20been=20flaky=20lately=20(#13?= =?UTF-8?q?03)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 10 ++++++++++ scripts/test.sh | 1 + 2 files changed, 11 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9126db93c5..c924e54ef4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -74,13 +74,23 @@ jobs: env: COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}-${{ matrix.pydantic-version }} CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }} + # TODO: if removing Smokeshow, and moving only to Codecov, remove this + # Upload files before running Codecov, as it generates an extra file coverage/coverage.xml, and that breaks coverage-combine - name: Store coverage files uses: actions/upload-artifact@v4 with: name: coverage-${{ matrix.python-version }}-${{ matrix.pydantic-version }} path: coverage include-hidden-files: true + - uses: codecov/codecov-action@v5 + with: + fail_ci_if_error: true + files: ./coverage.xml + name: codecov-umbrella + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true + # TODO: if removing Smokeshow, and moving only to Codecov, remove this coverage-combine: needs: - test diff --git a/scripts/test.sh b/scripts/test.sh index ff4b114b18..826ae903d5 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -7,3 +7,4 @@ coverage run -m pytest tests coverage combine coverage report coverage html +coverage xml From b8ded9b9ca0c1b6d1267008a7e1169d484ddd30c Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 21 Feb 2025 12:00:05 +0000 Subject: [PATCH 571/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index c341c54add..60f2c5ad88 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -22,6 +22,7 @@ ### Internal +* 👷 Add Codecov to CI, Smokeshow/Cloudflare has been flaky lately. PR [#1303](https://github.com/fastapi/sqlmodel/pull/1303) by [@tiangolo](https://github.com/tiangolo). * 👷 Add retries to Smokeshow. PR [#1302](https://github.com/fastapi/sqlmodel/pull/1302) by [@svlandeg](https://github.com/svlandeg). * ⬆ Bump astral-sh/setup-uv from 4 to 5. PR [#1249](https://github.com/fastapi/sqlmodel/pull/1249) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pillow from 10.3.0 to 11.0.0. PR [#1139](https://github.com/fastapi/sqlmodel/pull/1139) by [@dependabot[bot]](https://github.com/apps/dependabot). From a1fdc57271da1993a79587ecde12a5cb5b2255f3 Mon Sep 17 00:00:00 2001 From: Sofie Van Landeghem Date: Fri, 21 Feb 2025 15:45:27 +0100 Subject: [PATCH 572/906] =?UTF-8?q?=F0=9F=91=B7=20Revert=20"Add=20Codecov?= =?UTF-8?q?=20to=20CI,=20Smokeshow/Cloudflare=20has=20been=20flaky=20latel?= =?UTF-8?q?y=20(#1303)"=20(#1306)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert " 👷 Add Codecov to CI, Smokeshow/Cloudflare has been flaky lately (#1303)" This reverts commit 759220d5928af4a01c205a039eb9826456a475cd. --- .github/workflows/test.yml | 10 ---------- scripts/test.sh | 1 - 2 files changed, 11 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c924e54ef4..9126db93c5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -74,23 +74,13 @@ jobs: env: COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}-${{ matrix.pydantic-version }} CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }} - # TODO: if removing Smokeshow, and moving only to Codecov, remove this - # Upload files before running Codecov, as it generates an extra file coverage/coverage.xml, and that breaks coverage-combine - name: Store coverage files uses: actions/upload-artifact@v4 with: name: coverage-${{ matrix.python-version }}-${{ matrix.pydantic-version }} path: coverage include-hidden-files: true - - uses: codecov/codecov-action@v5 - with: - fail_ci_if_error: true - files: ./coverage.xml - name: codecov-umbrella - token: ${{ secrets.CODECOV_TOKEN }} - verbose: true - # TODO: if removing Smokeshow, and moving only to Codecov, remove this coverage-combine: needs: - test diff --git a/scripts/test.sh b/scripts/test.sh index 826ae903d5..ff4b114b18 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -7,4 +7,3 @@ coverage run -m pytest tests coverage combine coverage report coverage html -coverage xml From 8a35352bc56d3667b2b7f3697df858f50c7145c4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 21 Feb 2025 14:45:48 +0000 Subject: [PATCH 573/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 60f2c5ad88..486c0b264a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -22,6 +22,7 @@ ### Internal +* 👷 Revert "Add Codecov to CI, Smokeshow/Cloudflare has been flaky lately (#1303)". PR [#1306](https://github.com/fastapi/sqlmodel/pull/1306) by [@svlandeg](https://github.com/svlandeg). * 👷 Add Codecov to CI, Smokeshow/Cloudflare has been flaky lately. PR [#1303](https://github.com/fastapi/sqlmodel/pull/1303) by [@tiangolo](https://github.com/tiangolo). * 👷 Add retries to Smokeshow. PR [#1302](https://github.com/fastapi/sqlmodel/pull/1302) by [@svlandeg](https://github.com/svlandeg). * ⬆ Bump astral-sh/setup-uv from 4 to 5. PR [#1249](https://github.com/fastapi/sqlmodel/pull/1249) by [@dependabot[bot]](https://github.com/apps/dependabot). From ac9d1a514a144ad3fa6ca422dcbbe1bf2d0796fc Mon Sep 17 00:00:00 2001 From: Sofie Van Landeghem Date: Mon, 24 Feb 2025 10:18:24 +0100 Subject: [PATCH 574/906] =?UTF-8?q?=F0=9F=92=9A=20Fix=20CI=20test=20suite?= =?UTF-8?q?=20for=20Python=203.7=20(#1309)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9126db93c5..19e696188c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,9 +23,9 @@ env: jobs: test: - runs-on: ubuntu-latest strategy: matrix: + os: [ ubuntu-latest ] python-version: - "3.8" - "3.9" @@ -35,7 +35,15 @@ jobs: pydantic-version: - pydantic-v1 - pydantic-v2 + include: + - os: ubuntu-22.04 + python-version: "3.7" + pydantic-version: pydantic-v1 + - os: ubuntu-22.04 + python-version: "3.7" + pydantic-version: pydantic-v2 fail-fast: false + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Set up Python From ce22f2a50b485b81185b6d4b2873cbd4eb7af65b Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 24 Feb 2025 09:18:56 +0000 Subject: [PATCH 575/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 486c0b264a..f03d193782 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -22,6 +22,7 @@ ### Internal +* 💚 Fix CI test suite for Python 3.7. PR [#1309](https://github.com/fastapi/sqlmodel/pull/1309) by [@svlandeg](https://github.com/svlandeg). * 👷 Revert "Add Codecov to CI, Smokeshow/Cloudflare has been flaky lately (#1303)". PR [#1306](https://github.com/fastapi/sqlmodel/pull/1306) by [@svlandeg](https://github.com/svlandeg). * 👷 Add Codecov to CI, Smokeshow/Cloudflare has been flaky lately. PR [#1303](https://github.com/fastapi/sqlmodel/pull/1303) by [@tiangolo](https://github.com/tiangolo). * 👷 Add retries to Smokeshow. PR [#1302](https://github.com/fastapi/sqlmodel/pull/1302) by [@svlandeg](https://github.com/svlandeg). From c1936036c1a7f4f3581f78cf61338904c67fa664 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Feb 2025 11:22:59 +0100 Subject: [PATCH 576/906] =?UTF-8?q?=E2=AC=86=20Bump=20pypa/gh-action-pypi-?= =?UTF-8?q?publish=20from=201.12.3=20to=201.12.4=20(#1277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.12.3 to 1.12.4. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.12.3...v1.12.4) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sofie Van Landeghem --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 725928abb3..d4ec995ddf 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -34,4 +34,4 @@ jobs: TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }} run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.12.3 + uses: pypa/gh-action-pypi-publish@v1.12.4 From a82c3fe964e06e8c4ec6559d559eb930af53b98d Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 24 Feb 2025 10:23:21 +0000 Subject: [PATCH 577/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index f03d193782..717d2143aa 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -22,6 +22,7 @@ ### Internal +* ⬆ Bump pypa/gh-action-pypi-publish from 1.12.3 to 1.12.4. PR [#1277](https://github.com/fastapi/sqlmodel/pull/1277) by [@dependabot[bot]](https://github.com/apps/dependabot). * 💚 Fix CI test suite for Python 3.7. PR [#1309](https://github.com/fastapi/sqlmodel/pull/1309) by [@svlandeg](https://github.com/svlandeg). * 👷 Revert "Add Codecov to CI, Smokeshow/Cloudflare has been flaky lately (#1303)". PR [#1306](https://github.com/fastapi/sqlmodel/pull/1306) by [@svlandeg](https://github.com/svlandeg). * 👷 Add Codecov to CI, Smokeshow/Cloudflare has been flaky lately. PR [#1303](https://github.com/fastapi/sqlmodel/pull/1303) by [@tiangolo](https://github.com/tiangolo). From 1bd5f27ed4cb74d97ad9179d7b42d0aae7e873fb Mon Sep 17 00:00:00 2001 From: bubbletroubles <42738824+bubbletroubles@users.noreply.github.com> Date: Sat, 1 Mar 2025 00:58:52 +1100 Subject: [PATCH 578/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20documentation?= =?UTF-8?q?=20to=20refer=20to=20`list`=20instead=20of=20`List`=20(#1147)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sofie Van Landeghem Co-authored-by: svlandeg --- docs/tutorial/code-structure.md | 2 +- docs/tutorial/fastapi/response-model.md | 2 +- docs/tutorial/many-to-many/create-models-with-link.md | 4 ++-- .../define-relationships-attributes.md | 2 +- .../relationship-attributes/type-annotation-strings.md | 6 +++--- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/tutorial/code-structure.md b/docs/tutorial/code-structure.md index 386b5c8e4d..6e377b89e4 100644 --- a/docs/tutorial/code-structure.md +++ b/docs/tutorial/code-structure.md @@ -183,7 +183,7 @@ And this ends up *requiring* the same **circular imports** that are not supporte But these **type annotations** we want to declare are not needed at *runtime*. -In fact, remember that we used `List["Hero"]`, with a `"Hero"` in a string? +In fact, remember that we used `list["Hero"]`, with a `"Hero"` in a string? For Python, at runtime, that is **just a string**. diff --git a/docs/tutorial/fastapi/response-model.md b/docs/tutorial/fastapi/response-model.md index f5c0ab9f95..f9214332c6 100644 --- a/docs/tutorial/fastapi/response-model.md +++ b/docs/tutorial/fastapi/response-model.md @@ -38,7 +38,7 @@ For example, we can pass the same `Hero` **SQLModel** class (because it is also We can also use other type annotations, the same way we can use with Pydantic fields. For example, we can pass a list of `Hero`s. -First, we import `List` from `typing` and then we declare the `response_model` with `List[Hero]`: +To do so, we declare the `response_model` with `list[Hero]`: {* ./docs_src/tutorial/fastapi/response_model/tutorial001_py310.py ln[40:44] hl[40] *} diff --git a/docs/tutorial/many-to-many/create-models-with-link.md b/docs/tutorial/many-to-many/create-models-with-link.md index 69d0f96808..36a0e10e7f 100644 --- a/docs/tutorial/many-to-many/create-models-with-link.md +++ b/docs/tutorial/many-to-many/create-models-with-link.md @@ -28,7 +28,7 @@ Let's see the `Team` model, it's almost identical as before, but with a little c {* ./docs_src/tutorial/many_to_many/tutorial001_py310.py ln[9:14] hl[14] *} -The **relationship attribute `heroes`** is still a list of heroes, annotated as `List["Hero"]`. Again, we use `"Hero"` in quotes because we haven't declared that class yet by this point in the code (but as you know, editors and **SQLModel** understand that). +The **relationship attribute `heroes`** is still a list of heroes, annotated as `list["Hero"]`. Again, we use `"Hero"` in quotes because we haven't declared that class yet by this point in the code (but as you know, editors and **SQLModel** understand that). We use the same **`Relationship()`** function. @@ -46,7 +46,7 @@ We **removed** the previous `team_id` field (column) because now the relationshi The relationship attribute is now named **`teams`** instead of `team`, as now we support multiple teams. -It is no longer an `Optional[Team]` but a list of teams, annotated as **`List[Team]`**. +It is no longer an `Optional[Team]` but a list of teams, annotated as **`list[Team]`**. We are using the **`Relationship()`** here too. diff --git a/docs/tutorial/relationship-attributes/define-relationships-attributes.md b/docs/tutorial/relationship-attributes/define-relationships-attributes.md index 7839891988..2646082c38 100644 --- a/docs/tutorial/relationship-attributes/define-relationships-attributes.md +++ b/docs/tutorial/relationship-attributes/define-relationships-attributes.md @@ -86,7 +86,7 @@ And in the `Team` class, the `heroes` attribute is annotated as a list of `Hero` /// tip -There's a couple of things we'll check again in some of the next chapters, about the `List["Hero"]` and the `back_populates`. +There's a couple of things we'll check again in some of the next chapters, about the `list["Hero"]` and the `back_populates`. But for now, let's first see how to use these relationship attributes. diff --git a/docs/tutorial/relationship-attributes/type-annotation-strings.md b/docs/tutorial/relationship-attributes/type-annotation-strings.md index 026036a2f4..74b97f08d1 100644 --- a/docs/tutorial/relationship-attributes/type-annotation-strings.md +++ b/docs/tutorial/relationship-attributes/type-annotation-strings.md @@ -1,10 +1,10 @@ -## About the String in `List["Hero"]` +## About the String in `list["Hero"]` -In the first Relationship attribute, we declare it with `List["Hero"]`, putting the `Hero` in quotes instead of just normally there: +In the first Relationship attribute, we declare it with `list["Hero"]`, putting the `Hero` in quotes instead of just normally there: {* ./docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001_py310.py ln[1:19] hl[9] *} -What's that about? Can't we just write it normally as `List[Hero]`? +What's that about? Can't we just write it normally as `list[Hero]`? By that point, in that line in the code, the Python interpreter **doesn't know of any class `Hero`**, and if we put it just there, it would try to find it unsuccessfully, and then fail. 😭 From f46997ec6db7d662fb4b8be7d0bb74c6b3a2af41 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 28 Feb 2025 13:59:12 +0000 Subject: [PATCH 579/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 717d2143aa..a7c68735f7 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Docs +* 📝 Update documentation to refer to `list` instead of `List`. PR [#1147](https://github.com/fastapi/sqlmodel/pull/1147) by [@bubbletroubles](https://github.com/bubbletroubles). * ✏️ Fix typo in `databases.md`. PR [#1113](https://github.com/fastapi/sqlmodel/pull/1113) by [@radi-dev](https://github.com/radi-dev). * ✏️ Fix typo in `docs/tutorial/create-db-and-table.md`. PR [#1252](https://github.com/fastapi/sqlmodel/pull/1252) by [@ArianHamdi](https://github.com/ArianHamdi). * ✏️ Fix typo in `insert.md`. PR [#1256](https://github.com/fastapi/sqlmodel/pull/1256) by [@Noushadaliam](https://github.com/Noushadaliam). From 69a4504a3302b0afe7b716fa7a9349829d5fc930 Mon Sep 17 00:00:00 2001 From: Andrey Siunov Date: Fri, 28 Feb 2025 06:01:13 -0800 Subject: [PATCH 580/906] =?UTF-8?q?=F0=9F=90=9B=20Fix=20Pydantic=20version?= =?UTF-8?q?=20check=20for=20version=202.10.x=20onwards=20(#1255)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sofie Van Landeghem --- sqlmodel/_compat.py | 3 ++- sqlmodel/main.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index 4e80cdc374..d6b98aaca7 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -25,7 +25,8 @@ # Reassign variable to make it reexported for mypy PYDANTIC_VERSION = P_VERSION -IS_PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.") +PYDANTIC_MINOR_VERSION = tuple(int(i) for i in P_VERSION.split(".")[:2]) +IS_PYDANTIC_V2 = PYDANTIC_MINOR_VERSION[0] == 2 if TYPE_CHECKING: diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 3532e81a8e..61952be2f1 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -56,7 +56,7 @@ from ._compat import ( # type: ignore[attr-defined] IS_PYDANTIC_V2, - PYDANTIC_VERSION, + PYDANTIC_MINOR_VERSION, BaseConfig, ModelField, ModelMetaclass, @@ -874,7 +874,7 @@ def model_dump( warnings: Union[bool, Literal["none", "warn", "error"]] = True, serialize_as_any: bool = False, ) -> Dict[str, Any]: - if PYDANTIC_VERSION >= "2.7.0": + if PYDANTIC_MINOR_VERSION >= (2, 7): extra_kwargs: Dict[str, Any] = { "context": context, "serialize_as_any": serialize_as_any, From 850dd745f82deb3f69543afff0ea353c7ba89c92 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 28 Feb 2025 14:01:34 +0000 Subject: [PATCH 581/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index a7c68735f7..9581a64fd4 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Fixes + +* 🐛 Fix Pydantic version check for version 2.10.x onwards. PR [#1255](https://github.com/fastapi/sqlmodel/pull/1255) by [@asiunov](https://github.com/asiunov). + ### Refactors * 🚨 Fix types for new Pydantic. PR [#1131](https://github.com/fastapi/sqlmodel/pull/1131) by [@tiangolo](https://github.com/tiangolo). From 7a51870361b7bc99fa02516867ce4180b314aea3 Mon Sep 17 00:00:00 2001 From: Alan Bogarin Date: Fri, 28 Feb 2025 11:04:27 -0300 Subject: [PATCH 582/906] =?UTF-8?q?=F0=9F=90=9B=20Fix=20type=20annotation?= =?UTF-8?q?=20in=20`Field`=20constructor=20(#1304)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sofie Van Landeghem --- sqlmodel/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 61952be2f1..0fa2c111c0 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -341,7 +341,7 @@ def Field( regex: Optional[str] = None, discriminator: Optional[str] = None, repr: bool = True, - sa_column: Union[Column, UndefinedType] = Undefined, # type: ignore + sa_column: Union[Column[Any], UndefinedType] = Undefined, schema_extra: Optional[Dict[str, Any]] = None, ) -> Any: ... From 94d47ac784618189b7d4ed2e0392228152cd0249 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 28 Feb 2025 14:04:48 +0000 Subject: [PATCH 583/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 9581a64fd4..b72c219924 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Fixes +* 🐛 Fix type annotation in `Field` constructor. PR [#1304](https://github.com/fastapi/sqlmodel/pull/1304) by [@AlanBogarin](https://github.com/AlanBogarin). * 🐛 Fix Pydantic version check for version 2.10.x onwards. PR [#1255](https://github.com/fastapi/sqlmodel/pull/1255) by [@asiunov](https://github.com/asiunov). ### Refactors From b3e0f1cffecd61d8373b3e3fcd123dfba800fafa Mon Sep 17 00:00:00 2001 From: Sofie Van Landeghem Date: Fri, 28 Feb 2025 15:22:27 +0100 Subject: [PATCH 584/906] =?UTF-8?q?=F0=9F=A9=BA=20Take=20the=20GH=20badge?= =?UTF-8?q?=20only=20from=20pushes=20to=20the=20`main`=20branch=20(#1291)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +++--- docs/index.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index fc38789b7c..f8a32a3634 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness.

- - Test + + Test - Publish + Publish Coverage diff --git a/docs/index.md b/docs/index.md index 96062f0363..0feec81c69 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,11 +12,11 @@ SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness.

- - Test + + Test - Publish + Publish Coverage From 2ce6abf2bebf2aa478b1172a7c79556d98d36407 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 28 Feb 2025 14:23:55 +0000 Subject: [PATCH 585/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index b72c219924..dd0c2a8ba7 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -13,6 +13,7 @@ ### Docs +* 🩺 Take the GH badge only from pushes to the `main` branch. PR [#1291](https://github.com/fastapi/sqlmodel/pull/1291) by [@svlandeg](https://github.com/svlandeg). * 📝 Update documentation to refer to `list` instead of `List`. PR [#1147](https://github.com/fastapi/sqlmodel/pull/1147) by [@bubbletroubles](https://github.com/bubbletroubles). * ✏️ Fix typo in `databases.md`. PR [#1113](https://github.com/fastapi/sqlmodel/pull/1113) by [@radi-dev](https://github.com/radi-dev). * ✏️ Fix typo in `docs/tutorial/create-db-and-table.md`. PR [#1252](https://github.com/fastapi/sqlmodel/pull/1252) by [@ArianHamdi](https://github.com/ArianHamdi). From bf376f83caa8f3ee5820a1a906fadda7b0b10949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 28 Feb 2025 17:54:40 +0100 Subject: [PATCH 586/906] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.0.?= =?UTF-8?q?23?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 2 ++ sqlmodel/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index dd0c2a8ba7..a47d439c59 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest Changes +## 0.0.23 + ### Fixes * 🐛 Fix type annotation in `Field` constructor. PR [#1304](https://github.com/fastapi/sqlmodel/pull/1304) by [@AlanBogarin](https://github.com/AlanBogarin). diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index f62988f4ac..befc6d4eee 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.0.22" +__version__ = "0.0.23" # Re-export from SQLAlchemy from sqlalchemy.engine import create_engine as create_engine From b1349dae45dc6f28adae7601206be29e2f59e5a8 Mon Sep 17 00:00:00 2001 From: Sofie Van Landeghem Date: Thu, 6 Mar 2025 19:58:40 +0100 Subject: [PATCH 587/906] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Add=20support=20fo?= =?UTF-8?q?r=20Python=203.13=20(#1289)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 8 ++++++-- pyproject.toml | 1 + requirements-tests.txt | 6 ++++-- sqlmodel/main.py | 6 +++--- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 19e696188c..f842d51798 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,6 +32,7 @@ jobs: - "3.10" - "3.11" - "3.12" + - "3.13" pydantic-version: - pydantic-v1 - pydantic-v2 @@ -71,7 +72,10 @@ jobs: run: uv pip install --upgrade "pydantic>=1.10.0,<2.0.0" - name: Install Pydantic v2 if: matrix.pydantic-version == 'pydantic-v2' - run: uv pip install --upgrade "pydantic>=2.0.2,<3.0.0" "typing-extensions==4.6.1" + run: uv pip install --upgrade "pydantic>=2.0.2,<3.0.0" + - name: Pin typing-extensions for Python 3.7 + if: matrix.python-version == '3.7' + run: uv pip install --upgrade "typing-extensions==4.6.1" - name: Lint # Do not run on Python 3.7 as mypy behaves differently if: matrix.python-version != '3.7' && matrix.pydantic-version == 'pydantic-v2' @@ -97,7 +101,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: '3.13' - name: Setup uv uses: astral-sh/setup-uv@v5 with: diff --git a/pyproject.toml b/pyproject.toml index e3b70b5abd..bc8e9e2f6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Database", "Topic :: Database :: Database Engines/Servers", "Topic :: Internet", diff --git a/requirements-tests.txt b/requirements-tests.txt index 847ce90c4a..f378d8fa62 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -8,8 +8,10 @@ ruff ==0.6.2 fastapi >=0.103.2 httpx ==0.24.1 # TODO: upgrade when deprecating Python 3.7 -dirty-equals ==0.6.0 +dirty-equals ==0.6.0; python_version < "3.8" +dirty-equals ==0.9.0; python_version >= "3.8" jinja2 ==3.1.4 # Pin typing-extensions until Python 3.8 is deprecated or the issue with dirty-equals # is fixed, maybe fixed after dropping Python 3.7 and upgrading dirty-equals -typing-extensions ==4.6.1 +typing-extensions ==4.6.1; python_version < "3.8" +typing-extensions ==4.12.2; python_version >= "3.8" diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 0fa2c111c0..317ef6cca5 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -93,8 +93,8 @@ IncEx: TypeAlias = Union[ Set[int], Set[str], - Mapping[int, Union["IncEx", Literal[True]]], - Mapping[str, Union["IncEx", Literal[True]]], + Mapping[int, Union["IncEx", bool]], + Mapping[str, Union["IncEx", bool]], ] OnDeleteType = Literal["CASCADE", "SET NULL", "RESTRICT"] @@ -479,7 +479,7 @@ def Relationship( class SQLModelMetaclass(ModelMetaclass, DeclarativeMeta): __sqlmodel_relationships__: Dict[str, RelationshipInfo] model_config: SQLModelConfig - model_fields: Dict[str, FieldInfo] + model_fields: Dict[str, FieldInfo] # type: ignore[assignment] __config__: Type[SQLModelConfig] __fields__: Dict[str, ModelField] # type: ignore[assignment] From 262610c3b04afc7f4a3aae2204741f8a4319563a Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 6 Mar 2025 18:59:00 +0000 Subject: [PATCH 588/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index a47d439c59..7120c250e0 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Upgrades + +* ⬆️ Add support for Python 3.13. PR [#1289](https://github.com/fastapi/sqlmodel/pull/1289) by [@svlandeg](https://github.com/svlandeg). + ## 0.0.23 ### Fixes From 1e853aaf6d882b261b46f08d504a8b87a4a7ad91 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Mar 2025 04:21:45 +0100 Subject: [PATCH 589/906] =?UTF-8?q?=E2=AC=86=20Bump=20ruff=20from=200.6.2?= =?UTF-8?q?=20to=200.9.6=20(#1294)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ⬆ Bump ruff from 0.6.2 to 0.9.6 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.6.2 to 0.9.6. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.6.2...0.9.6) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * format with ruff 0.9.6 * 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks * also bump in pre-commit config * 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sofie Van Landeghem Co-authored-by: svlandeg Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- requirements-tests.txt | 2 +- sqlmodel/main.py | 6 ++---- .../test_fastapi/test_update/test_tutorial001.py | 8 ++++---- .../test_update/test_tutorial001_py310.py | 8 ++++---- .../test_update/test_tutorial001_py39.py | 8 ++++---- .../test_fastapi/test_update/test_tutorial002.py | 12 ++++++------ .../test_update/test_tutorial002_py310.py | 12 ++++++------ .../test_update/test_tutorial002_py39.py | 12 ++++++------ 9 files changed, 34 insertions(+), 36 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4b1b10a68f..c385ccfd3c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.5 + rev: v0.9.6 hooks: - id: ruff args: diff --git a/requirements-tests.txt b/requirements-tests.txt index f378d8fa62..b33f3bf1b7 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -3,7 +3,7 @@ pytest >=7.0.1,<8.0.0 coverage[toml] >=6.2,<8.0 mypy ==1.4.1 -ruff ==0.6.2 +ruff ==0.9.6 # For FastAPI tests fastapi >=0.103.2 httpx ==0.24.1 diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 317ef6cca5..45a41997fe 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -134,8 +134,7 @@ def __init__(self, default: Any = Undefined, **kwargs: Any) -> None: ) if primary_key is not Undefined: raise RuntimeError( - "Passing primary_key is not supported when " - "also passing a sa_column" + "Passing primary_key is not supported when also passing a sa_column" ) if nullable is not Undefined: raise RuntimeError( @@ -143,8 +142,7 @@ def __init__(self, default: Any = Undefined, **kwargs: Any) -> None: ) if foreign_key is not Undefined: raise RuntimeError( - "Passing foreign_key is not supported when " - "also passing a sa_column" + "Passing foreign_key is not supported when also passing a sa_column" ) if ondelete is not Undefined: raise RuntimeError( diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py index 0856b24676..6f3856912e 100644 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py @@ -49,16 +49,16 @@ def test_tutorial(clear_sqlmodel): data = response.json() assert response.status_code == 200, response.text assert data["name"] == hero2_data["name"], "The name should not be set to none" - assert ( - data["secret_name"] == "Spider-Youngster" - ), "The secret name should be updated" + assert data["secret_name"] == "Spider-Youngster", ( + "The secret name should be updated" + ) response = client.patch(f"/heroes/{hero3_id}", json={"age": None}) data = response.json() assert response.status_code == 200, response.text assert data["name"] == hero3_data["name"] assert data["age"] is None, ( - "A field should be updatable to None, even if " "that's the default" + "A field should be updatable to None, even if that's the default" ) response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py310.py index d79b2ecea3..119634dc1e 100644 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py310.py @@ -52,16 +52,16 @@ def test_tutorial(clear_sqlmodel): data = response.json() assert response.status_code == 200, response.text assert data["name"] == hero2_data["name"], "The name should not be set to none" - assert ( - data["secret_name"] == "Spider-Youngster" - ), "The secret name should be updated" + assert data["secret_name"] == "Spider-Youngster", ( + "The secret name should be updated" + ) response = client.patch(f"/heroes/{hero3_id}", json={"age": None}) data = response.json() assert response.status_code == 200, response.text assert data["name"] == hero3_data["name"] assert data["age"] is None, ( - "A field should be updatable to None, even if " "that's the default" + "A field should be updatable to None, even if that's the default" ) response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py39.py index 1be81dec2e..455480f735 100644 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py39.py @@ -52,16 +52,16 @@ def test_tutorial(clear_sqlmodel): data = response.json() assert response.status_code == 200, response.text assert data["name"] == hero2_data["name"], "The name should not be set to none" - assert ( - data["secret_name"] == "Spider-Youngster" - ), "The secret name should be updated" + assert data["secret_name"] == "Spider-Youngster", ( + "The secret name should be updated" + ) response = client.patch(f"/heroes/{hero3_id}", json={"age": None}) data = response.json() assert response.status_code == 200, response.text assert data["name"] == hero3_data["name"] assert data["age"] is None, ( - "A field should be updatable to None, even if " "that's the default" + "A field should be updatable to None, even if that's the default" ) response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial002.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial002.py index 32e343ed61..2a929f6dae 100644 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial002.py +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial002.py @@ -80,9 +80,9 @@ def test_tutorial(clear_sqlmodel): data = response.json() assert response.status_code == 200, response.text assert data["name"] == hero2_data["name"], "The name should not be set to none" - assert ( - data["secret_name"] == "Spider-Youngster" - ), "The secret name should be updated" + assert data["secret_name"] == "Spider-Youngster", ( + "The secret name should be updated" + ) assert "password" not in data assert "hashed_password" not in data with Session(mod.engine) as session: @@ -95,9 +95,9 @@ def test_tutorial(clear_sqlmodel): data = response.json() assert response.status_code == 200, response.text assert data["name"] == hero3_data["name"] - assert ( - data["age"] is None - ), "A field should be updatable to None, even if that's the default" + assert data["age"] is None, ( + "A field should be updatable to None, even if that's the default" + ) assert "password" not in data assert "hashed_password" not in data with Session(mod.engine) as session: diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py310.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py310.py index b05f5b2133..7617f14996 100644 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py310.py +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py310.py @@ -83,9 +83,9 @@ def test_tutorial(clear_sqlmodel): data = response.json() assert response.status_code == 200, response.text assert data["name"] == hero2_data["name"], "The name should not be set to none" - assert ( - data["secret_name"] == "Spider-Youngster" - ), "The secret name should be updated" + assert data["secret_name"] == "Spider-Youngster", ( + "The secret name should be updated" + ) assert "password" not in data assert "hashed_password" not in data with Session(mod.engine) as session: @@ -98,9 +98,9 @@ def test_tutorial(clear_sqlmodel): data = response.json() assert response.status_code == 200, response.text assert data["name"] == hero3_data["name"] - assert ( - data["age"] is None - ), "A field should be updatable to None, even if that's the default" + assert data["age"] is None, ( + "A field should be updatable to None, even if that's the default" + ) assert "password" not in data assert "hashed_password" not in data with Session(mod.engine) as session: diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py39.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py39.py index 807e33421a..dc788a29f7 100644 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py39.py +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py39.py @@ -83,9 +83,9 @@ def test_tutorial(clear_sqlmodel): data = response.json() assert response.status_code == 200, response.text assert data["name"] == hero2_data["name"], "The name should not be set to none" - assert ( - data["secret_name"] == "Spider-Youngster" - ), "The secret name should be updated" + assert data["secret_name"] == "Spider-Youngster", ( + "The secret name should be updated" + ) assert "password" not in data assert "hashed_password" not in data with Session(mod.engine) as session: @@ -98,9 +98,9 @@ def test_tutorial(clear_sqlmodel): data = response.json() assert response.status_code == 200, response.text assert data["name"] == hero3_data["name"] - assert ( - data["age"] is None - ), "A field should be updatable to None, even if that's the default" + assert data["age"] is None, ( + "A field should be updatable to None, even if that's the default" + ) assert "password" not in data assert "hashed_password" not in data with Session(mod.engine) as session: From 8d212e86787d94ca7ca35a93b8b64f915689efcb Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 7 Mar 2025 03:22:03 +0000 Subject: [PATCH 590/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 7120c250e0..8e8d8daeee 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,6 +6,10 @@ * ⬆️ Add support for Python 3.13. PR [#1289](https://github.com/fastapi/sqlmodel/pull/1289) by [@svlandeg](https://github.com/svlandeg). +### Internal + +* ⬆ Bump ruff from 0.6.2 to 0.9.6. PR [#1294](https://github.com/fastapi/sqlmodel/pull/1294) by [@dependabot[bot]](https://github.com/apps/dependabot). + ## 0.0.23 ### Fixes From 3242bdb3c8330807365f425a0a5b17fc6f105965 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 7 Mar 2025 03:26:23 +0000 Subject: [PATCH 591/906] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#1114)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ⬆ [pre-commit.ci] pre-commit autoupdate updates: - [github.com/pre-commit/pre-commit-hooks: v4.6.0 → v5.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.6.0...v5.0.0) - [github.com/astral-sh/ruff-pre-commit: v0.6.5 → v0.9.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.5...v0.9.9) * 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c385ccfd3c..232e0baf9b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ default_language_version: python: python3.10 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: check-added-large-files - id: check-toml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.6 + rev: v0.9.9 hooks: - id: ruff args: From 8fc8bdf3d059e849c170377dfff84d3b548bddd5 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 7 Mar 2025 03:26:57 +0000 Subject: [PATCH 592/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 8e8d8daeee..6f6524b287 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1114](https://github.com/fastapi/sqlmodel/pull/1114) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump ruff from 0.6.2 to 0.9.6. PR [#1294](https://github.com/fastapi/sqlmodel/pull/1294) by [@dependabot[bot]](https://github.com/apps/dependabot). ## 0.0.23 From de78b8c7bc26038f9f047cc66dd3a49c65864df0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 7 Mar 2025 06:40:27 +0100 Subject: [PATCH 593/906] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.0.?= =?UTF-8?q?24?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 2 ++ sqlmodel/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 6f6524b287..09ee515266 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest Changes +## 0.0.24 + ### Upgrades * ⬆️ Add support for Python 3.13. PR [#1289](https://github.com/fastapi/sqlmodel/pull/1289) by [@svlandeg](https://github.com/svlandeg). diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index befc6d4eee..28bb305237 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.0.23" +__version__ = "0.0.24" # Re-export from SQLAlchemy from sqlalchemy.engine import create_engine as create_engine From b66ec8eed375f8cb049610757bb92236e45cf122 Mon Sep 17 00:00:00 2001 From: Sofie Van Landeghem Date: Sun, 16 Mar 2025 21:32:07 +0100 Subject: [PATCH 594/906] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Drop=20support=20f?= =?UTF-8?q?or=20Python=203.7,=20require=20Python=203.8=20or=20above=20(#13?= =?UTF-8?q?16)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 13 +------------ docs/tutorial/automatic-id-none-refresh.md | 2 +- docs/tutorial/create-db-and-table.md | 2 +- docs/tutorial/delete.md | 2 +- docs/tutorial/fastapi/tests.md | 2 +- docs/tutorial/insert.md | 4 ++-- docs/tutorial/select.md | 2 +- docs/tutorial/update.md | 6 +++--- pyproject.toml | 3 +-- requirements-docs.txt | 3 +-- requirements-tests.txt | 9 ++------- 11 files changed, 15 insertions(+), 33 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f842d51798..57dba1c286 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,13 +36,6 @@ jobs: pydantic-version: - pydantic-v1 - pydantic-v2 - include: - - os: ubuntu-22.04 - python-version: "3.7" - pydantic-version: pydantic-v1 - - os: ubuntu-22.04 - python-version: "3.7" - pydantic-version: pydantic-v2 fail-fast: false runs-on: ${{ matrix.os }} steps: @@ -73,12 +66,8 @@ jobs: - name: Install Pydantic v2 if: matrix.pydantic-version == 'pydantic-v2' run: uv pip install --upgrade "pydantic>=2.0.2,<3.0.0" - - name: Pin typing-extensions for Python 3.7 - if: matrix.python-version == '3.7' - run: uv pip install --upgrade "typing-extensions==4.6.1" - name: Lint - # Do not run on Python 3.7 as mypy behaves differently - if: matrix.python-version != '3.7' && matrix.pydantic-version == 'pydantic-v2' + if: matrix.pydantic-version == 'pydantic-v2' run: bash scripts/lint.sh - run: mkdir coverage - name: Test diff --git a/docs/tutorial/automatic-id-none-refresh.md b/docs/tutorial/automatic-id-none-refresh.md index 464227f60b..7c7983c3d3 100644 --- a/docs/tutorial/automatic-id-none-refresh.md +++ b/docs/tutorial/automatic-id-none-refresh.md @@ -342,7 +342,7 @@ And as we created the **engine** with `echo=True`, we can see the SQL statements //// -//// tab | Python 3.7+ +//// tab | Python 3.8+ ```Python {!./docs_src/tutorial/automatic_id_none_refresh/tutorial002.py!} diff --git a/docs/tutorial/create-db-and-table.md b/docs/tutorial/create-db-and-table.md index c1076d5887..288e2bb20b 100644 --- a/docs/tutorial/create-db-and-table.md +++ b/docs/tutorial/create-db-and-table.md @@ -562,7 +562,7 @@ Now, let's give the code a final look: //// -//// tab | Python 3.7+ +//// tab | Python 3.8+ ```{.python .annotate} {!./docs_src/tutorial/create_db_and_table/tutorial003.py!} diff --git a/docs/tutorial/delete.md b/docs/tutorial/delete.md index fbf65eb3ea..b0eaf6788b 100644 --- a/docs/tutorial/delete.md +++ b/docs/tutorial/delete.md @@ -227,7 +227,7 @@ Now let's review all that code: //// -//// tab | Python 3.7+ +//// tab | Python 3.8+ ```{ .python .annotate hl_lines="72-90" } {!./docs_src/tutorial/delete/tutorial002.py!} diff --git a/docs/tutorial/fastapi/tests.md b/docs/tutorial/fastapi/tests.md index 9c5255b7bf..ed4f91bcd6 100644 --- a/docs/tutorial/fastapi/tests.md +++ b/docs/tutorial/fastapi/tests.md @@ -356,7 +356,7 @@ Now we can run the tests with `pytest` and see the results: $ pytest ============= test session starts ============== -platform linux -- Python 3.7.5, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 +platform linux -- Python 3.10.0, pytest-7.4.4, pluggy-1.5.0 rootdir: /home/user/code/sqlmodel-tutorial collected 7 items diff --git a/docs/tutorial/insert.md b/docs/tutorial/insert.md index 2110e4c0b2..01977de35b 100644 --- a/docs/tutorial/insert.md +++ b/docs/tutorial/insert.md @@ -39,7 +39,7 @@ This is the code we had to create the database and table, nothing new here: //// -//// tab | Python 3.7+ +//// tab | Python 3.8+ ```{.python .annotate hl_lines="22" } {!./docs_src/tutorial/create_db_and_table/tutorial003.py[ln:1-20]!} @@ -343,7 +343,7 @@ Let's focus on the new code: //// -//// tab | Python 3.7+ +//// tab | Python 3.8+ ```{.python .annotate } {!./docs_src/tutorial/insert/tutorial003.py!} diff --git a/docs/tutorial/select.md b/docs/tutorial/select.md index d130b514fd..cd996932c6 100644 --- a/docs/tutorial/select.md +++ b/docs/tutorial/select.md @@ -273,7 +273,7 @@ Let's review the code up to this point: //// -//// tab | Python 3.7+ +//// tab | Python 3.8+ ```{ .python .annotate } {!./docs_src/tutorial/select/tutorial002.py!} diff --git a/docs/tutorial/update.md b/docs/tutorial/update.md index 497a2c1a78..8e60536815 100644 --- a/docs/tutorial/update.md +++ b/docs/tutorial/update.md @@ -236,7 +236,7 @@ Now let's review all that code: //// -//// tab | Python 3.7+ +//// tab | Python 3.8+ ```{ .python .annotate hl_lines="44-55" } {!./docs_src/tutorial/update/tutorial002.py!} @@ -272,7 +272,7 @@ This also means that you can update several fields (attributes, columns) at once //// -//// tab | Python 3.7+ +//// tab | Python 3.8+ ```{ .python .annotate hl_lines="15-17 19-21 23" } # Code above omitted 👆 @@ -296,7 +296,7 @@ This also means that you can update several fields (attributes, columns) at once //// -//// tab | Python 3.7+ +//// tab | Python 3.8+ ```Python {!./docs_src/tutorial/update/tutorial004.py!} diff --git a/pyproject.toml b/pyproject.toml index bc8e9e2f6e..766b055819 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "sqlmodel" dynamic = ["version"] description = "SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness." readme = "README.md" -requires-python = ">=3.7" +requires-python = ">=3.8" authors = [ { name = "Sebastián Ramírez", email = "tiangolo@gmail.com" }, ] @@ -20,7 +20,6 @@ classifiers = [ "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", diff --git a/requirements-docs.txt b/requirements-docs.txt index da15671f50..318103451b 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -11,8 +11,7 @@ pillow==11.0.0 # For image processing by Material for MkDocs cairosvg==2.7.1 # mkdocstrings[python]==0.25.1 -# Enable griffe-typingdoc once dropping Python 3.7 and upgrading typing-extensions -# griffe-typingdoc==0.2.5 +griffe-typingdoc==0.2.5 # For griffe, it formats with black typer == 0.12.3 mkdocs-macros-plugin==1.0.5 diff --git a/requirements-tests.txt b/requirements-tests.txt index b33f3bf1b7..2ed86fac8d 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -7,11 +7,6 @@ ruff ==0.9.6 # For FastAPI tests fastapi >=0.103.2 httpx ==0.24.1 -# TODO: upgrade when deprecating Python 3.7 -dirty-equals ==0.6.0; python_version < "3.8" -dirty-equals ==0.9.0; python_version >= "3.8" +dirty-equals ==0.9.0 jinja2 ==3.1.4 -# Pin typing-extensions until Python 3.8 is deprecated or the issue with dirty-equals -# is fixed, maybe fixed after dropping Python 3.7 and upgrading dirty-equals -typing-extensions ==4.6.1; python_version < "3.8" -typing-extensions ==4.12.2; python_version >= "3.8" +typing-extensions ==4.12.2 From 3f7681ae5a1356b18fc23747f62a07cf75d31fb3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 16 Mar 2025 20:32:27 +0000 Subject: [PATCH 595/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 09ee515266..7fa3d86a98 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Upgrades + +* ⬆️ Drop support for Python 3.7, require Python 3.8 or above. PR [#1316](https://github.com/fastapi/sqlmodel/pull/1316) by [@svlandeg](https://github.com/svlandeg). + ## 0.0.24 ### Upgrades From 924e480bffadc067cc8d9a0558dacd00a6929cdd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 08:22:27 +0100 Subject: [PATCH 596/906] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#1319)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.9.9 → v0.11.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.9...v0.11.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 232e0baf9b..b52e89ff8b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.9 + rev: v0.11.0 hooks: - id: ruff args: From b11c6cfaba7ec142dc02d61af2ea918b2f48f702 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 18 Mar 2025 07:22:45 +0000 Subject: [PATCH 597/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 7fa3d86a98..70fe72f044 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,6 +6,10 @@ * ⬆️ Drop support for Python 3.7, require Python 3.8 or above. PR [#1316](https://github.com/fastapi/sqlmodel/pull/1316) by [@svlandeg](https://github.com/svlandeg). +### Internal + +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1319](https://github.com/fastapi/sqlmodel/pull/1319) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). + ## 0.0.24 ### Upgrades From c4b07fd80a2366658505b92e74a95bb5a4191ae9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 08:22:57 +0100 Subject: [PATCH 598/906] =?UTF-8?q?=E2=AC=86=20Bump=20jinja2=20from=203.1.?= =?UTF-8?q?4=20to=203.1.6=20(#1317)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.4 to 3.1.6. - [Release notes](https://github.com/pallets/jinja/releases) - [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/jinja/compare/3.1.4...3.1.6) --- updated-dependencies: - dependency-name: jinja2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 2ed86fac8d..81f0d68165 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -8,5 +8,5 @@ ruff ==0.9.6 fastapi >=0.103.2 httpx ==0.24.1 dirty-equals ==0.9.0 -jinja2 ==3.1.4 +jinja2 ==3.1.6 typing-extensions ==4.12.2 From e7744e566fa085b67fbc65f94afc50b35b1b8f20 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 18 Mar 2025 07:23:16 +0000 Subject: [PATCH 599/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 70fe72f044..ac774a2a58 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* ⬆ Bump jinja2 from 3.1.4 to 3.1.6. PR [#1317](https://github.com/fastapi/sqlmodel/pull/1317) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1319](https://github.com/fastapi/sqlmodel/pull/1319) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). ## 0.0.24 From 26b67e28844f1536e07c5d32ab0b852842426f8f Mon Sep 17 00:00:00 2001 From: sylvain hellin Date: Mon, 31 Mar 2025 10:55:22 +0200 Subject: [PATCH 600/906] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Update=20`docs/vir?= =?UTF-8?q?tual-environments.md`=20(#1321)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/virtual-environments.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/virtual-environments.md b/docs/virtual-environments.md index ca48de91d6..131cd53d76 100644 --- a/docs/virtual-environments.md +++ b/docs/virtual-environments.md @@ -668,7 +668,7 @@ After activating the virtual environment, the `PATH` variable would look somethi /home/user/code/awesome-project/.venv/bin:/usr/bin:/bin:/usr/sbin:/sbin ``` -That means that the system will now start looking first look for programs in: +That means that the system will now start looking first for programs in: ```plaintext /home/user/code/awesome-project/.venv/bin From aeeaa857b7ac583d4a3cbd3a06311b465084e6a0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 31 Mar 2025 08:55:41 +0000 Subject: [PATCH 601/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index ac774a2a58..79e88d4c70 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,6 +6,10 @@ * ⬆️ Drop support for Python 3.7, require Python 3.8 or above. PR [#1316](https://github.com/fastapi/sqlmodel/pull/1316) by [@svlandeg](https://github.com/svlandeg). +### Docs + +* ✏️ Update `docs/virtual-environments.md`. PR [#1321](https://github.com/fastapi/sqlmodel/pull/1321) by [@sylvainHellin](https://github.com/sylvainHellin). + ### Internal * ⬆ Bump jinja2 from 3.1.4 to 3.1.6. PR [#1317](https://github.com/fastapi/sqlmodel/pull/1317) by [@dependabot[bot]](https://github.com/apps/dependabot). From f419fbccd6bddfaea251e9bd3152b744797e483d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 31 Mar 2025 10:55:50 +0200 Subject: [PATCH 602/906] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#1327)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.11.0 → v0.11.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.0...v0.11.2) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b52e89ff8b..6aa7c458d6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.0 + rev: v0.11.2 hooks: - id: ruff args: From 6c0410ea915ff0605b339db25b53f87aa03ded2f Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 31 Mar 2025 08:56:10 +0000 Subject: [PATCH 603/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 79e88d4c70..8033a67867 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -12,6 +12,7 @@ ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1327](https://github.com/fastapi/sqlmodel/pull/1327) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump jinja2 from 3.1.4 to 3.1.6. PR [#1317](https://github.com/fastapi/sqlmodel/pull/1317) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1319](https://github.com/fastapi/sqlmodel/pull/1319) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From 4b5ad42c238b91591b2eae274ef26ba3607dde4f Mon Sep 17 00:00:00 2001 From: Sofie Van Landeghem Date: Sat, 26 Apr 2025 21:04:09 +0200 Subject: [PATCH 604/906] =?UTF-8?q?=F0=9F=92=9A=20Fix=20linting=20in=20CI?= =?UTF-8?q?=20(#1340)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- .github/workflows/test.yml | 2 +- sqlmodel/_compat.py | 9 ++++++++- sqlmodel/main.py | 15 ++++++++++----- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 57dba1c286..b812f3da46 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -67,7 +67,7 @@ jobs: if: matrix.pydantic-version == 'pydantic-v2' run: uv pip install --upgrade "pydantic>=2.0.2,<3.0.0" - name: Lint - if: matrix.pydantic-version == 'pydantic-v2' + if: matrix.pydantic-version == 'pydantic-v2' && matrix.python-version != '3.8' run: bash scripts/lint.sh - run: mkdir coverage - name: Test diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index d6b98aaca7..38dd501c4a 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -103,7 +103,14 @@ def set_config_value( model.model_config[parameter] = value # type: ignore[literal-required] def get_model_fields(model: InstanceOrType[BaseModel]) -> Dict[str, "FieldInfo"]: - return model.model_fields + # TODO: refactor the usage of this function to always pass the class + # not the instance, and then remove this extra check + # this is for compatibility with Pydantic v3 + if isinstance(model, type): + use_model = model + else: + use_model = model.__class__ + return use_model.model_fields def get_fields_set( object: InstanceOrType["SQLModel"], diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 45a41997fe..38c85915aa 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -477,7 +477,7 @@ def Relationship( class SQLModelMetaclass(ModelMetaclass, DeclarativeMeta): __sqlmodel_relationships__: Dict[str, RelationshipInfo] model_config: SQLModelConfig - model_fields: Dict[str, FieldInfo] # type: ignore[assignment] + model_fields: ClassVar[Dict[str, FieldInfo]] __config__: Type[SQLModelConfig] __fields__: Dict[str, ModelField] # type: ignore[assignment] @@ -839,7 +839,7 @@ def __tablename__(cls) -> str: return cls.__name__.lower() @classmethod - def model_validate( + def model_validate( # type: ignore[override] cls: Type[_TSQLModel], obj: Any, *, @@ -863,20 +863,25 @@ def model_dump( mode: Union[Literal["json", "python"], str] = "python", include: Union[IncEx, None] = None, exclude: Union[IncEx, None] = None, - context: Union[Dict[str, Any], None] = None, - by_alias: bool = False, + context: Union[Any, None] = None, + by_alias: Union[bool, None] = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, round_trip: bool = False, warnings: Union[bool, Literal["none", "warn", "error"]] = True, + fallback: Union[Callable[[Any], Any], None] = None, serialize_as_any: bool = False, ) -> Dict[str, Any]: + if PYDANTIC_MINOR_VERSION < (2, 11): + by_alias = by_alias or False if PYDANTIC_MINOR_VERSION >= (2, 7): extra_kwargs: Dict[str, Any] = { "context": context, "serialize_as_any": serialize_as_any, } + if PYDANTIC_MINOR_VERSION >= (2, 11): + extra_kwargs["fallback"] = fallback else: extra_kwargs = {} if IS_PYDANTIC_V2: @@ -896,7 +901,7 @@ def model_dump( return super().dict( include=include, exclude=exclude, - by_alias=by_alias, + by_alias=by_alias or False, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, exclude_none=exclude_none, From f42b4445201b271eadd1a1e2046ae61c1e30acfe Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 26 Apr 2025 19:04:30 +0000 Subject: [PATCH 605/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 8033a67867..eb93c5ddd3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -12,6 +12,7 @@ ### Internal +* 💚 Fix linting in CI. PR [#1340](https://github.com/fastapi/sqlmodel/pull/1340) by [@svlandeg](https://github.com/svlandeg). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1327](https://github.com/fastapi/sqlmodel/pull/1327) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump jinja2 from 3.1.4 to 3.1.6. PR [#1317](https://github.com/fastapi/sqlmodel/pull/1317) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1319](https://github.com/fastapi/sqlmodel/pull/1319) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From 2c388a30450d65a4d20715f5bab7819884268af5 Mon Sep 17 00:00:00 2001 From: Peder Bergebakken Sundt Date: Sun, 27 Apr 2025 13:31:49 +0200 Subject: [PATCH 606/906] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Update=20`tests/te?= =?UTF-8?q?st=5Fselect=5Fgen.py`,=20pass=20environment=20variables,=20need?= =?UTF-8?q?ed=20for=20NixOS=20nixpkgs=20(#969)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- tests/test_select_gen.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_select_gen.py b/tests/test_select_gen.py index 6d578f7708..e14200d513 100644 --- a/tests/test_select_gen.py +++ b/tests/test_select_gen.py @@ -1,3 +1,4 @@ +import os import subprocess import sys from pathlib import Path @@ -9,9 +10,11 @@ @needs_py39 def test_select_gen() -> None: + env = os.environ.copy() + env["CHECK_JINJA"] = "1" result = subprocess.run( [sys.executable, "scripts/generate_select.py"], - env={"CHECK_JINJA": "1"}, + env=env, check=True, cwd=root_path, capture_output=True, From ed96b1a338ca97a95c7cc053da33c6b684705fe1 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 27 Apr 2025 11:32:06 +0000 Subject: [PATCH 607/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index eb93c5ddd3..107c16beed 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -12,6 +12,7 @@ ### Internal +* ♻️ Update `tests/test_select_gen.py`, pass environment variables, needed for NixOS nixpkgs. PR [#969](https://github.com/fastapi/sqlmodel/pull/969) by [@pbsds](https://github.com/pbsds). * 💚 Fix linting in CI. PR [#1340](https://github.com/fastapi/sqlmodel/pull/1340) by [@svlandeg](https://github.com/svlandeg). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1327](https://github.com/fastapi/sqlmodel/pull/1327) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump jinja2 from 3.1.4 to 3.1.6. PR [#1317](https://github.com/fastapi/sqlmodel/pull/1317) by [@dependabot[bot]](https://github.com/apps/dependabot). From 7aaf20a2576a81c275522835706ded767328f37e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 27 Apr 2025 13:36:32 +0200 Subject: [PATCH 608/906] =?UTF-8?q?=E2=AC=86=20Update=20pytest=20requireme?= =?UTF-8?q?nt=20from=20<8.0.0,>=3D7.0.1=20to=20>=3D7.0.1,<9.0.0=20(#1022)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates the requirements on [pytest](https://github.com/pytest-dev/pytest) to permit the latest version. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.0.1...8.2.2) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 81f0d68165..ec87a3b262 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,6 +1,6 @@ -e . -r requirements-docs-tests.txt -pytest >=7.0.1,<8.0.0 +pytest >=7.0.1,<9.0.0 coverage[toml] >=6.2,<8.0 mypy ==1.4.1 ruff ==0.9.6 From 42bec01134ddff5bcccf377ed28298d157e44ba7 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 27 Apr 2025 11:36:49 +0000 Subject: [PATCH 609/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 107c16beed..8c72f2570e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -12,6 +12,7 @@ ### Internal +* ⬆ Update pytest requirement from <8.0.0,>=7.0.1 to >=7.0.1,<9.0.0. PR [#1022](https://github.com/fastapi/sqlmodel/pull/1022) by [@dependabot[bot]](https://github.com/apps/dependabot). * ♻️ Update `tests/test_select_gen.py`, pass environment variables, needed for NixOS nixpkgs. PR [#969](https://github.com/fastapi/sqlmodel/pull/969) by [@pbsds](https://github.com/pbsds). * 💚 Fix linting in CI. PR [#1340](https://github.com/fastapi/sqlmodel/pull/1340) by [@svlandeg](https://github.com/svlandeg). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1327](https://github.com/fastapi/sqlmodel/pull/1327) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From a9eb68efc38cf67b5afa1552eb51e96e9027e142 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 27 Apr 2025 11:46:30 +0000 Subject: [PATCH 610/906] =?UTF-8?q?=E2=AC=86=20Bump=20astral-sh/setup-uv?= =?UTF-8?q?=20from=205=20to=206=20(#1348)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 5 to 6. - [Release notes](https://github.com/astral-sh/setup-uv/releases) - [Commits](https://github.com/astral-sh/setup-uv/compare/v5...v6) --- updated-dependencies: - dependency-name: astral-sh/setup-uv dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-docs.yml | 2 +- .github/workflows/deploy-docs.yml | 2 +- .github/workflows/smokeshow.yml | 2 +- .github/workflows/test.yml | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 827d1518e6..ac1076c1ac 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -59,7 +59,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v6 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 01e0346d23..765a992cbd 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -29,7 +29,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v6 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index 5575d44abb..979ae0264e 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -21,7 +21,7 @@ jobs: with: python-version: '3.9' - name: Setup uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v6 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b812f3da46..003c7cab17 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,7 +45,7 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Setup uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v6 with: version: "0.4.15" enable-cache: true @@ -92,7 +92,7 @@ jobs: with: python-version: '3.13' - name: Setup uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v6 with: version: "0.4.15" enable-cache: true From ce14ff714ed86d1da738835de6292e6ccc67a25c Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 27 Apr 2025 11:46:54 +0000 Subject: [PATCH 611/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 8c72f2570e..bdde422ce6 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -12,6 +12,7 @@ ### Internal +* ⬆ Bump astral-sh/setup-uv from 5 to 6. PR [#1348](https://github.com/fastapi/sqlmodel/pull/1348) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Update pytest requirement from <8.0.0,>=7.0.1 to >=7.0.1,<9.0.0. PR [#1022](https://github.com/fastapi/sqlmodel/pull/1022) by [@dependabot[bot]](https://github.com/apps/dependabot). * ♻️ Update `tests/test_select_gen.py`, pass environment variables, needed for NixOS nixpkgs. PR [#969](https://github.com/fastapi/sqlmodel/pull/969) by [@pbsds](https://github.com/pbsds). * 💚 Fix linting in CI. PR [#1340](https://github.com/fastapi/sqlmodel/pull/1340) by [@svlandeg](https://github.com/svlandeg). From 55b462dc0874ccb1e8c3f7d052ffebce8d408223 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 27 Apr 2025 11:47:36 +0000 Subject: [PATCH 612/906] =?UTF-8?q?=E2=AC=86=20Bump=20httpx=20from=200.24.?= =?UTF-8?q?1=20to=200.28.1=20(#1238)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [httpx](https://github.com/encode/httpx) from 0.24.1 to 0.28.1. - [Release notes](https://github.com/encode/httpx/releases) - [Changelog](https://github.com/encode/httpx/blob/master/CHANGELOG.md) - [Commits](https://github.com/encode/httpx/compare/0.24.1...0.28.1) --- updated-dependencies: - dependency-name: httpx dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-github-actions.txt | 2 +- requirements-tests.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-github-actions.txt b/requirements-github-actions.txt index a6dace544f..5c3e02d8ae 100644 --- a/requirements-github-actions.txt +++ b/requirements-github-actions.txt @@ -1,5 +1,5 @@ PyGithub>=2.3.0,<3.0.0 pydantic>=2.5.3,<3.0.0 pydantic-settings>=2.1.0,<3.0.0 -httpx>=0.27.0,<0.28.0 +httpx>=0.27.0,<0.29.0 smokeshow diff --git a/requirements-tests.txt b/requirements-tests.txt index ec87a3b262..935f47cdab 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -6,7 +6,7 @@ mypy ==1.4.1 ruff ==0.9.6 # For FastAPI tests fastapi >=0.103.2 -httpx ==0.24.1 +httpx ==0.28.1 dirty-equals ==0.9.0 jinja2 ==3.1.6 typing-extensions ==4.12.2 From 990f8f407f48eb24be57567c75e79e74e29b5dcd Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 27 Apr 2025 11:47:52 +0000 Subject: [PATCH 613/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index bdde422ce6..4dca5845b2 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -12,6 +12,7 @@ ### Internal +* ⬆ Bump httpx from 0.24.1 to 0.28.1. PR [#1238](https://github.com/fastapi/sqlmodel/pull/1238) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump astral-sh/setup-uv from 5 to 6. PR [#1348](https://github.com/fastapi/sqlmodel/pull/1348) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Update pytest requirement from <8.0.0,>=7.0.1 to >=7.0.1,<9.0.0. PR [#1022](https://github.com/fastapi/sqlmodel/pull/1022) by [@dependabot[bot]](https://github.com/apps/dependabot). * ♻️ Update `tests/test_select_gen.py`, pass environment variables, needed for NixOS nixpkgs. PR [#969](https://github.com/fastapi/sqlmodel/pull/969) by [@pbsds](https://github.com/pbsds). From bc67b582b682d41a7eb2ba7d3b65892e7afb46dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joel=20P=C3=A9rez=20Izquierdo?= Date: Sun, 27 Apr 2025 14:02:41 +0100 Subject: [PATCH 614/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20FastAPI=20tutor?= =?UTF-8?q?ial=20docs=20to=20use=20the=20new=20`model.sqlmodel=5Fupdate()`?= =?UTF-8?q?=20instead=20of=20old=20`setattr()`=20(#1117)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/contributing.md | 2 +- docs/tutorial/fastapi/teams.md | 2 +- docs_src/tutorial/fastapi/app_testing/tutorial001/main.py | 3 +-- .../tutorial/fastapi/app_testing/tutorial001_py310/main.py | 3 +-- .../tutorial/fastapi/app_testing/tutorial001_py39/main.py | 3 +-- docs_src/tutorial/fastapi/delete/tutorial001.py | 3 +-- docs_src/tutorial/fastapi/delete/tutorial001_py310.py | 3 +-- docs_src/tutorial/fastapi/delete/tutorial001_py39.py | 3 +-- docs_src/tutorial/fastapi/relationships/tutorial001.py | 6 ++---- .../tutorial/fastapi/relationships/tutorial001_py310.py | 6 ++---- docs_src/tutorial/fastapi/relationships/tutorial001_py39.py | 6 ++---- .../tutorial/fastapi/session_with_dependency/tutorial001.py | 3 +-- .../fastapi/session_with_dependency/tutorial001_py310.py | 3 +-- .../fastapi/session_with_dependency/tutorial001_py39.py | 3 +-- docs_src/tutorial/fastapi/teams/tutorial001.py | 6 ++---- docs_src/tutorial/fastapi/teams/tutorial001_py310.py | 6 ++---- docs_src/tutorial/fastapi/teams/tutorial001_py39.py | 6 ++---- 17 files changed, 23 insertions(+), 44 deletions(-) diff --git a/docs/contributing.md b/docs/contributing.md index e3693c5a8d..8228bc4132 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -63,7 +63,7 @@ There is a script that you can run locally to test all the code and generate cov

diff --git a/docs/tutorial/fastapi/teams.md b/docs/tutorial/fastapi/teams.md index 93d38829f7..4f07fb1981 100644 --- a/docs/tutorial/fastapi/teams.md +++ b/docs/tutorial/fastapi/teams.md @@ -46,7 +46,7 @@ Let's now add the **path operations** for teams. These are equivalent and very similar to the **path operations** for the **heroes** we had before, so we don't have to go over the details for each one, let's check the code. -{* ./docs_src/tutorial/fastapi/teams/tutorial001_py310.py ln[136:190] hl[136:142,145:153,156:161,164:180,183:190] *} +{* ./docs_src/tutorial/fastapi/teams/tutorial001_py310.py ln[135:188] hl[135:141,144:152,155:160,163:178,181:188] *} ## Using Relationships Attributes diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py b/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py index f46d8b078e..f0a2559467 100644 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py @@ -88,8 +88,7 @@ def update_hero( if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") hero_data = hero.model_dump(exclude_unset=True) - for key, value in hero_data.items(): - setattr(db_hero, key, value) + db_hero.sqlmodel_update(hero_data) session.add(db_hero) session.commit() session.refresh(db_hero) diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/main.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/main.py index 702eba722b..84da9fd610 100644 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/main.py +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py310/main.py @@ -86,8 +86,7 @@ def update_hero( if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") hero_data = hero.model_dump(exclude_unset=True) - for key, value in hero_data.items(): - setattr(db_hero, key, value) + db_hero.sqlmodel_update(hero_data) session.add(db_hero) session.commit() session.refresh(db_hero) diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/main.py b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/main.py index efdfd8ee6c..e7371d84e3 100644 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/main.py +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001_py39/main.py @@ -88,8 +88,7 @@ def update_hero( if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") hero_data = hero.model_dump(exclude_unset=True) - for key, value in hero_data.items(): - setattr(db_hero, key, value) + db_hero.sqlmodel_update(hero_data) session.add(db_hero) session.commit() session.refresh(db_hero) diff --git a/docs_src/tutorial/fastapi/delete/tutorial001.py b/docs_src/tutorial/fastapi/delete/tutorial001.py index 1871ab1707..977882c4c2 100644 --- a/docs_src/tutorial/fastapi/delete/tutorial001.py +++ b/docs_src/tutorial/fastapi/delete/tutorial001.py @@ -80,8 +80,7 @@ def update_hero(hero_id: int, hero: HeroUpdate): if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") hero_data = hero.model_dump(exclude_unset=True) - for key, value in hero_data.items(): - setattr(db_hero, key, value) + db_hero.sqlmodel_update(hero_data) session.add(db_hero) session.commit() session.refresh(db_hero) diff --git a/docs_src/tutorial/fastapi/delete/tutorial001_py310.py b/docs_src/tutorial/fastapi/delete/tutorial001_py310.py index 0dd0c889fa..f7de4019d2 100644 --- a/docs_src/tutorial/fastapi/delete/tutorial001_py310.py +++ b/docs_src/tutorial/fastapi/delete/tutorial001_py310.py @@ -78,8 +78,7 @@ def update_hero(hero_id: int, hero: HeroUpdate): if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") hero_data = hero.model_dump(exclude_unset=True) - for key, value in hero_data.items(): - setattr(db_hero, key, value) + db_hero.sqlmodel_update(hero_data) session.add(db_hero) session.commit() session.refresh(db_hero) diff --git a/docs_src/tutorial/fastapi/delete/tutorial001_py39.py b/docs_src/tutorial/fastapi/delete/tutorial001_py39.py index 9ab3056c82..5d5f099abb 100644 --- a/docs_src/tutorial/fastapi/delete/tutorial001_py39.py +++ b/docs_src/tutorial/fastapi/delete/tutorial001_py39.py @@ -80,8 +80,7 @@ def update_hero(hero_id: int, hero: HeroUpdate): if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") hero_data = hero.model_dump(exclude_unset=True) - for key, value in hero_data.items(): - setattr(db_hero, key, value) + db_hero.sqlmodel_update(hero_data) session.add(db_hero) session.commit() session.refresh(db_hero) diff --git a/docs_src/tutorial/fastapi/relationships/tutorial001.py b/docs_src/tutorial/fastapi/relationships/tutorial001.py index ac8a557cfc..59b44730ba 100644 --- a/docs_src/tutorial/fastapi/relationships/tutorial001.py +++ b/docs_src/tutorial/fastapi/relationships/tutorial001.py @@ -126,8 +126,7 @@ def update_hero( if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") hero_data = hero.model_dump(exclude_unset=True) - for key, value in hero_data.items(): - setattr(db_hero, key, value) + db_hero.sqlmodel_update(hero_data) session.add(db_hero) session.commit() session.refresh(db_hero) @@ -183,8 +182,7 @@ def update_team( if not db_team: raise HTTPException(status_code=404, detail="Team not found") team_data = team.model_dump(exclude_unset=True) - for key, value in team_data.items(): - setattr(db_team, key, value) + db_team.sqlmodel_update(team_data) session.add(db_team) session.commit() session.refresh(db_team) diff --git a/docs_src/tutorial/fastapi/relationships/tutorial001_py310.py b/docs_src/tutorial/fastapi/relationships/tutorial001_py310.py index 5110b158b0..47c2e5f0bc 100644 --- a/docs_src/tutorial/fastapi/relationships/tutorial001_py310.py +++ b/docs_src/tutorial/fastapi/relationships/tutorial001_py310.py @@ -124,8 +124,7 @@ def update_hero( if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") hero_data = hero.model_dump(exclude_unset=True) - for key, value in hero_data.items(): - setattr(db_hero, key, value) + db_hero.sqlmodel_update(hero_data) session.add(db_hero) session.commit() session.refresh(db_hero) @@ -181,8 +180,7 @@ def update_team( if not db_team: raise HTTPException(status_code=404, detail="Team not found") team_data = team.model_dump(exclude_unset=True) - for key, value in team_data.items(): - setattr(db_team, key, value) + db_team.sqlmodel_update(team_data) session.add(db_team) session.commit() session.refresh(db_team) diff --git a/docs_src/tutorial/fastapi/relationships/tutorial001_py39.py b/docs_src/tutorial/fastapi/relationships/tutorial001_py39.py index a4e953c4d7..1cfa298b8b 100644 --- a/docs_src/tutorial/fastapi/relationships/tutorial001_py39.py +++ b/docs_src/tutorial/fastapi/relationships/tutorial001_py39.py @@ -126,8 +126,7 @@ def update_hero( if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") hero_data = hero.model_dump(exclude_unset=True) - for key, value in hero_data.items(): - setattr(db_hero, key, value) + db_hero.sqlmodel_update(hero_data) session.add(db_hero) session.commit() session.refresh(db_hero) @@ -183,8 +182,7 @@ def update_team( if not db_team: raise HTTPException(status_code=404, detail="Team not found") team_data = team.model_dump(exclude_unset=True) - for key, value in team_data.items(): - setattr(db_team, key, value) + db_team.sqlmodel_update(team_data) session.add(db_team) session.commit() session.refresh(db_team) diff --git a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py index f46d8b078e..f0a2559467 100644 --- a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py +++ b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py @@ -88,8 +88,7 @@ def update_hero( if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") hero_data = hero.model_dump(exclude_unset=True) - for key, value in hero_data.items(): - setattr(db_hero, key, value) + db_hero.sqlmodel_update(hero_data) session.add(db_hero) session.commit() session.refresh(db_hero) diff --git a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py index 702eba722b..84da9fd610 100644 --- a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py +++ b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py310.py @@ -86,8 +86,7 @@ def update_hero( if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") hero_data = hero.model_dump(exclude_unset=True) - for key, value in hero_data.items(): - setattr(db_hero, key, value) + db_hero.sqlmodel_update(hero_data) session.add(db_hero) session.commit() session.refresh(db_hero) diff --git a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py index efdfd8ee6c..e7371d84e3 100644 --- a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py +++ b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001_py39.py @@ -88,8 +88,7 @@ def update_hero( if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") hero_data = hero.model_dump(exclude_unset=True) - for key, value in hero_data.items(): - setattr(db_hero, key, value) + db_hero.sqlmodel_update(hero_data) session.add(db_hero) session.commit() session.refresh(db_hero) diff --git a/docs_src/tutorial/fastapi/teams/tutorial001.py b/docs_src/tutorial/fastapi/teams/tutorial001.py index 4289221ff5..49dd83065a 100644 --- a/docs_src/tutorial/fastapi/teams/tutorial001.py +++ b/docs_src/tutorial/fastapi/teams/tutorial001.py @@ -117,8 +117,7 @@ def update_hero( if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") hero_data = hero.model_dump(exclude_unset=True) - for key, value in hero_data.items(): - setattr(db_hero, key, value) + db_hero.sqlmodel_update(hero_data) session.add(db_hero) session.commit() session.refresh(db_hero) @@ -174,8 +173,7 @@ def update_team( if not db_team: raise HTTPException(status_code=404, detail="Team not found") team_data = team.model_dump(exclude_unset=True) - for key, value in team_data.items(): - setattr(db_team, key, value) + db_team.sqlmodel_update(team_data) session.add(db_team) session.commit() session.refresh(db_team) diff --git a/docs_src/tutorial/fastapi/teams/tutorial001_py310.py b/docs_src/tutorial/fastapi/teams/tutorial001_py310.py index 630ae4f98a..b78f059e12 100644 --- a/docs_src/tutorial/fastapi/teams/tutorial001_py310.py +++ b/docs_src/tutorial/fastapi/teams/tutorial001_py310.py @@ -115,8 +115,7 @@ def update_hero( if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") hero_data = hero.model_dump(exclude_unset=True) - for key, value in hero_data.items(): - setattr(db_hero, key, value) + db_hero.sqlmodel_update(hero_data) session.add(db_hero) session.commit() session.refresh(db_hero) @@ -172,8 +171,7 @@ def update_team( if not db_team: raise HTTPException(status_code=404, detail="Team not found") team_data = team.model_dump(exclude_unset=True) - for key, value in team_data.items(): - setattr(db_team, key, value) + db_team.sqlmodel_update(team_data) session.add(db_team) session.commit() session.refresh(db_team) diff --git a/docs_src/tutorial/fastapi/teams/tutorial001_py39.py b/docs_src/tutorial/fastapi/teams/tutorial001_py39.py index 661e435373..928ec70976 100644 --- a/docs_src/tutorial/fastapi/teams/tutorial001_py39.py +++ b/docs_src/tutorial/fastapi/teams/tutorial001_py39.py @@ -117,8 +117,7 @@ def update_hero( if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") hero_data = hero.model_dump(exclude_unset=True) - for key, value in hero_data.items(): - setattr(db_hero, key, value) + db_hero.sqlmodel_update(hero_data) session.add(db_hero) session.commit() session.refresh(db_hero) @@ -174,8 +173,7 @@ def update_team( if not db_team: raise HTTPException(status_code=404, detail="Team not found") team_data = team.model_dump(exclude_unset=True) - for key, value in team_data.items(): - setattr(db_team, key, value) + db_team.sqlmodel_update(team_data) session.add(db_team) session.commit() session.refresh(db_team) From 49dd5ffde11dc8b3a9d91162ae251b22fe30dcd1 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 27 Apr 2025 13:02:59 +0000 Subject: [PATCH 615/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 4dca5845b2..3f08c34d49 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Docs +* 📝 Update FastAPI tutorial docs to use the new `model.sqlmodel_update()` instead of old `setattr()`. PR [#1117](https://github.com/fastapi/sqlmodel/pull/1117) by [@jpizquierdo](https://github.com/jpizquierdo). * ✏️ Update `docs/virtual-environments.md`. PR [#1321](https://github.com/fastapi/sqlmodel/pull/1321) by [@sylvainHellin](https://github.com/sylvainHellin). ### Internal From dd03d05889c9dc10521c0632a6e2c5c5e6ca8a56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 27 Apr 2025 15:19:59 +0200 Subject: [PATCH 616/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20install=20and?= =?UTF-8?q?=20usage=20with=20FastAPI=20CLI=20in=20FastAPI=20tutorial=20(#1?= =?UTF-8?q?350)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tutorial/fastapi/simple-hero-api.md | 40 +++++++----------------- 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/docs/tutorial/fastapi/simple-hero-api.md b/docs/tutorial/fastapi/simple-hero-api.md index c6f27f4baf..163fa281b1 100644 --- a/docs/tutorial/fastapi/simple-hero-api.md +++ b/docs/tutorial/fastapi/simple-hero-api.md @@ -8,10 +8,6 @@ The first step is to install FastAPI. FastAPI is the framework to create the **web API**. -But we also need another type of program to run it, it is called a "**server**". We will use **Uvicorn** for that. And we will install Uvicorn with its *standard* dependencies. - -Then install FastAPI. - Make sure you create a [virtual environment](../../virtual-environments.md){.internal-link target=_blank}, activate it, and then install them, for example with:
@@ -138,60 +134,48 @@ In this simple example, we just create the new sessions manually in the **path o In future examples later we will use a FastAPI Dependency to get the **session**, being able to share it with other dependencies and being able to replace it during testing. 🤓 -## Run the **FastAPI** Application +## Run the **FastAPI** Server in Development Mode Now we are ready to run the FastAPI application. Put all that code in a file called `main.py`. -Then run it with **Uvicorn**: +Then run it with the `fastapi` CLI, in development mode:
```console -$ uvicorn main:app +$ fastapi dev main.py INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. ```
/// info -The command `uvicorn main:app` refers to: - -* `main`: the file `main.py` (the Python "module"). -* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. +The `fastapi` command uses Uvicorn underneath. /// -### Uvicorn `--reload` +When you use `fastapi dev` it starts Uvicorn with the option to reload automatically every time you make a change to the code, this way you will be able to develop faster. 🤓 -During development (and only during development), you can also add the option `--reload` to Uvicorn. +## Run the **FastAPI** Server in Production Mode -It will restart the server every time you make a change to the code, this way you will be able to develop faster. 🤓 +The development mode should not be used in production, as it includes automatic reload by default it consumes much more resources than necessary, and it would be more error prone, etc. + +For production, use `fastapi run` instead of `fastapi dev`:
```console -$ uvicorn main:app --reload +$ fastapi run main.py -INFO: Will watch for changes in these directories: ['/home/user/code/sqlmodel-tutorial'] -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. +INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit) ```
-Just remember to never use `--reload` in production, as it consumes much more resources than necessary, would be more error prone, etc. - ## Check the API docs UI Now you can go to that URL in your browser `http://127.0.0.1:8000`. We didn't create a *path operation* for the root path `/`, so that URL alone will only show a "Not Found" error... that "Not Found" error is produced by your FastAPI application. @@ -212,7 +196,7 @@ And then you can get them back with the **Read Heroes** *path operation*: ## Check the Database -Now you can terminate that Uvicorn server by going back to the terminal and pressing Ctrl+C. +Now you can terminate that server program by going back to the terminal and pressing Ctrl+C. And then, you can open **DB Browser for SQLite** and check the database, to explore the data and confirm that it indeed saved the heroes. 🎉 From 0e5e19773cc440442ad5eb01b36afad994f8004d Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 27 Apr 2025 13:20:16 +0000 Subject: [PATCH 617/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 3f08c34d49..644e6b7262 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Docs +* 📝 Update install and usage with FastAPI CLI in FastAPI tutorial. PR [#1350](https://github.com/fastapi/sqlmodel/pull/1350) by [@tiangolo](https://github.com/tiangolo). * 📝 Update FastAPI tutorial docs to use the new `model.sqlmodel_update()` instead of old `setattr()`. PR [#1117](https://github.com/fastapi/sqlmodel/pull/1117) by [@jpizquierdo](https://github.com/jpizquierdo). * ✏️ Update `docs/virtual-environments.md`. PR [#1321](https://github.com/fastapi/sqlmodel/pull/1321) by [@sylvainHellin](https://github.com/sylvainHellin). From 61523864f1f4fc60040f8ae369353460bedc7fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 27 Apr 2025 20:53:37 +0200 Subject: [PATCH 618/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20all=20docs=20re?= =?UTF-8?q?ferences=20to=20`Optional`=20to=20use=20the=20new=20syntax=20in?= =?UTF-8?q?=20Python=203.10,=20e.g.=20`int=20|=20None`=20(#1351)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/DISCUSSION_TEMPLATE/questions.yml | 6 ++-- README.md | 22 ++++++--------- docs/db-to-code.md | 4 +-- docs/img/index/autocompletion01.png | Bin 41667 -> 90886 bytes docs/img/index/autocompletion02.png | Bin 58615 -> 165165 bytes docs/img/index/inline-errors01.png | Bin 42369 -> 103453 bytes docs/index.md | 22 ++++++--------- docs/tutorial/automatic-id-none-refresh.md | 10 +++---- .../tutorial/connect/create-connected-rows.md | 2 +- docs/tutorial/create-db-and-table.md | 26 +++++++++--------- docs/tutorial/fastapi/multiple-models.md | 10 +++---- docs/tutorial/fastapi/update.md | 6 ++-- .../many-to-many/create-models-with-link.md | 2 +- .../define-relationships-attributes.md | 6 ++-- docs/tutorial/where.md | 2 +- 15 files changed, 52 insertions(+), 66 deletions(-) diff --git a/.github/DISCUSSION_TEMPLATE/questions.yml b/.github/DISCUSSION_TEMPLATE/questions.yml index 97902a658e..524d5c2d08 100644 --- a/.github/DISCUSSION_TEMPLATE/questions.yml +++ b/.github/DISCUSSION_TEMPLATE/questions.yml @@ -64,16 +64,14 @@ body: If I (or someone) can copy it, run it, and see it right away, there's a much higher chance I (or someone) will be able to help you. placeholder: | - from typing import Optional - from sqlmodel import Field, Session, SQLModel, create_engine class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) name: str secret_name: str - age: Optional[int] = None + age: int | None = None hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") diff --git a/README.md b/README.md index f8a32a3634..712167ffd6 100644 --- a/README.md +++ b/README.md @@ -105,16 +105,14 @@ And you want it to have this data: Then you could create a **SQLModel** model like this: ```Python -from typing import Optional - from sqlmodel import Field, SQLModel class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) name: str secret_name: str - age: Optional[int] = None + age: int | None = None ``` That class `Hero` is a **SQLModel** model, the equivalent of a SQL table in Python code. @@ -149,17 +147,15 @@ And **inline errors**: You can learn a lot more about **SQLModel** by quickly following the **tutorial**, but if you need a taste right now of how to put all that together and save to the database, you can do this: -```Python hl_lines="18 21 23-27" -from typing import Optional - +```Python hl_lines="16 19 21-25" from sqlmodel import Field, Session, SQLModel, create_engine class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) name: str secret_name: str - age: Optional[int] = None + age: int | None = None hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") @@ -185,17 +181,15 @@ That will save a **SQLite** database with the 3 heroes. Then you could write queries to select from that same database, for example with: -```Python hl_lines="15-18" -from typing import Optional - +```Python hl_lines="13-17" from sqlmodel import Field, Session, SQLModel, create_engine, select class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) name: str secret_name: str - age: Optional[int] = None + age: int | None = None engine = create_engine("sqlite:///database.db") diff --git a/docs/db-to-code.md b/docs/db-to-code.md index 3d289d75fa..53a8d35886 100644 --- a/docs/db-to-code.md +++ b/docs/db-to-code.md @@ -252,10 +252,10 @@ For example this class is part of that **Object** Oriented Programming: ```Python class Hero(SQLModel): - id: Optional[int] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) name: str secret_name: str - age: Optional[int] = None + age: int | None = None ``` * **Relational**: refers to the **SQL Databases**. Remember that they are also called **Relational Databases**, because each of those tables is also called a "**relation**"? That's where the "**Relational**" comes from. diff --git a/docs/img/index/autocompletion01.png b/docs/img/index/autocompletion01.png index 1a47940b3c03fde8099c65dceb7d2d8ab52473f9..cba2649fe9ed532e1b9d654f7f8a5ab2b1c5245c 100644 GIT binary patch literal 90886 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 XM7+C%FEC4h=E3x?r7`*IhcW*GJgan| literal 41667 zcmb5VWpEu$)~#7$CX3mU#mvmi%xp0;vqiR;nVFfHnVBWavY45rtGByv-07JwCT4zB zR92qcCo4}@t!M3hLKWo15nyp(0RTXdln_w@0MJta04aqA{cO?XzdiaqfH?_Csz5_S zukXnJ{yar?7S(W8wlj5h`|fB0D4V-DJDWHf1hi(_ zwGY}WB=>6oELDsB1+Tq|!-bknxlQAuC4S?1=W*HElJ3dlB5w?reGc)j5dzH^a0zG< zl2($3mv8>&=qQbg_bz`_Bd zAY>uJq>>^Zk}~hM%uM_wh<`fqeJyizk@TQ!8u+_QWMj=Vbiyyq2ZslZLA4}Rx$vCk+yc$m#=DynCj z_A@3GJpv=$_rMqC?20XZqlGL^-0Mca3&B~<7K_RYf#_xy&M#l$#+aqHkC>PvM3L&h z%-t4s`=O@6R1?jvv$I|MMrzC$1=+8|?$qAZ*c3_QCa-xHZ{#Lk{{9mE9MRnw6d6!h z47 zenD()6plF<0{}Vq#|2u8rGswLuuRx}q}xA_2zdTN1duNm%M5J0gd<?>g5f2EUMCn4(;}=Xba6+w(R}-o|i9kJJ)ggN0 zI?0M@G@fyFT|b}1e`1k+k0%#r+mfzGYnFaN z*FLkOn|2~ZS*plF&&7DF{X4OXXte*ZtG2BX@=|Q}_H}7#0siJLZa<2B(P`?C8jk0B zR&KV*SaQZ--VNbcH_oIHrl3|P^2=0`U1u(N?nI;V)l$iE{BH$L1adnHrBr>jKES~A zdT>`?Y|<@fK~(0c?KfEp8Mw`tS|z4;

tx^hz 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 diff --git a/docs/img/index/autocompletion02.png b/docs/img/index/autocompletion02.png index effa22ee85888001c2bd34644b92c880b9425765..5918e1d2002e348e682551e22425db3a879ab8d5 100644 GIT binary patch literal 165165 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%0DM9zJuw!-pW;9&NuhE~?BqP)x zUaH~^eAC3Su9sS`9c^c|MW#Ez@c>`4ljQ45TbC@)bnMVG;cWfCi71z+o85$#SFpjY z;=&*{FxI*Hac}@I_j#W0lxst;@PBhXKfT}Hd`o;ddWsz(xVbBZL}D&rn&++f|u)67Guy1;^Y0) zc9IjeuS#mCOryfTBz;D6gx7;8)0+3!-2fAo>=cqbHSN6j4U!l>s~J4PQ`Q=^8*BL*!qV?8l9d>A=fx4@_Sf&qnnqsNlFa+yjdi^>PG%K@8AZ zmipTyG^)d=X-wVUz|REBYJ9~yc&g8oJ~sydq%(S~%f?w;X{uWzoU7SopWpggfwlkuY4GKM&Z}xG*{MXE;U-kE zK|;VCvE`nND}I^4@jLlmUMHFX-;HXB>dERYJ?s*W@mkUzs zeatz2@P2{TF;Rq9TgydKLjW_Z!=f;_Za3G__8?yWeDfaPTiy4#Yn7%G=R<+SAm(N* z%zHTLdL@Dkr|q}70G;=7&VFvt->R2tl-F}rQsalD7F4FYW2Gyyti&TQ}K@v~mHif5SY&VbTc_gHDPF0l6`TSGgy{YThz-)68P>m$Zg z`cAfwX?Q&Ccisnoc}g5i+wi%z5{6-h_Z(}6ASg0z`_EOBSrM6lPJ3PKu0JhKTvpgH z?An>L6T$b~qqXt+#ycMY;V0gB#qlW(@zKsT*qsE)v9_eSQvy2n!}|q$tBVNM-{Ks8 zmR=*6~4)QA7Gk`XKMVEs#H5kvBD_=Li7DmaF3@k`0sZgY|TkS-Z`mxUqY?pa+ zLVdStXY4Lf`e>|Y^Pc6a3vDlf<5VKR2_R~RmtO8V!W{wMU-=h~Q@Dmh`m+bGFnR@W z=*Z<;;zt2blNmjc~TalT@Xc zU~R|YVM!P~&RPJ{c!1{o{vaEWTh7r<%v|{6#Lgt2Gk1%ch9`2F6-cYmFRk?k` z!~nh4et!bQ#7*evPxvhBG?zG~+?u-3$sWDctt+@tKCCX^eu0(YXk)UHaTF<2GqtQN zcNhJ+9kMdnD~0w*d2IthI?T z4X!2sd1c#Ym{mQ73uB3^7VrI-Iui!clUnjms!BJ8#(`6MQxqFc)cPy$QI4{3u4r&zZ_QRO zZ!+A*BLWv%VP1*55lM{b9mO8y!9+;Q(cp*jqQ)D)_n^;^xV!r}EzC)RHF|`tM)#7N zFI2;OjiVrMY?C$G6NoMON$@R8)xXoa@>Q*@UZ>5!e5XuHHBtX<1aYQTjwg-mafDRy z?GgSv(f6Y(n91Os!c{vxqFe)U{>u4BQ*DC3&z9>i?ixV(lkG(fu@|2C{Bg&flqUl# zTYi6AL4ZfRz4jfS!~U+pu||`}<6R_8Y2no#TYFO->Gn^R?J)*M0}tkU%sQ|u4JW?` zN8T)ke4Fg??TI9Vd+gdICb&~7oh!Xw)YWnMw-qwqQyjN94X!A~96!XCpuC;hRIY1N z8>#9bD_uO(U3Bz^Pc!|>{C8^^t(pu5Z!5&qMj|yGwurrK&Hh611&0l#hL^Dj7aMJ) zuMVI2Tq@o6x)yc+Sxb1)S>PljXgdE z$TViVCoOH(xd`0A~&zghU8t zb8FOEJ9M`u>>YaQUT3d24j>vAuIrV9{@w5Zr|;h~tew|k#VofL(7}C%z)cZvHtJjG z9!I`kR|=KxMhtjCujYB-h)<4`!EnEJb=ha~gYMEg^iL$*(Zy=U$q%1T9{WaY6_!Ca zS=T`!kC!q?^;Y_8Aa=X_7^2Vo@RgXTb%yiWr%qGtSMbkU^FxrRy0v7jB4FYkM;nRy zgrzH8e)AvjdSv^Nc=>|}^sn)SHuhicKjhDN>tx#!5bCt2T2f!Y0=~craLU7LFA=1U zbmoAr;8ng!Q3JP=+JGpPqpBATbsgxHHbajlN7sLl(7D@|ze=M1bYb;2x*zJzxaSpr z6(QhXcQ7;lHLg=1u7aY=_-#WspR6=fjdy^D-Yy9BuQit%Wp=r|xPG8Y;OQwc*HE3*GZRY&%RU(h74H zm|l16_oerGiTkyG5WugLIogKjvA`HFIfjp3$XE)9T$1neh{5?c^TFk;TkC*if(r#S zMmI)@u>h|6wzCGK-05KPup|k|A|$XDc)Y4M!j6xXDn_?f8^;y6Zv%hcV6h4ql|Pxl zzG}C{j8;&K%W6m(0so2mKK7oQ>C109pK?3Dw@3rq?4-a)(mdwNjPx10BzUahA@|Im z(jVpFk@6?p7Tnks!kMk9MIICnpg$teJi`BzwaX4ip=JUVIqFx_zMt* zckCHNPoo_%KY?L6*{Od-o4b~|Qaz=zX3mgB0l2Wgqc>@V!;QFTCWBYxZ}r49dr zrlx+Ro_nR`^>&w;K%#S}PR4=TEE(|0j|~sUYfs~pQL3xSKV;hD9af|-*J>z{Rt;P9 z`91S;hJyZwUY#;kVS7+PJG4VMI0(O};l%V0YaxKS_$4NpF!#7VvDGoEUgR)LaztN7 zjGMd9$wc7k`-u3t$z;vXrI;tHC6wK~xyH=jVt|p9^GTvI`rKRPgT=E!)wRVwxvBu4 zl(_!CuU`Qsuq*aG!8^B1KUmp&>{y=c-#6w|#`hj6pRr-#0N`Njl4q7-<#cFMYwWf# z1*`jQzg(v93MX0GmHXctOO6w@@`&<##KpX)e&MovQ}`EPl{4#q9LvpoXr0M-*yWbQMTl8BU&#Mr?k$7kT9!3YS(3%F zEVNj*n3)+Yw3u14n3v8NT2<9q znO|mpDT6*0l7Q9UFO1A$`1vFjKdkxD!FQW+E!eE0(})@lNsa9-XBRuOLo~!Z19Auo zUgrn2x598w)7#(ywdbAyiqHj4{H3wgw!x@IYiJ}AujD`>RYLiS_Ypkxha9T_trwES zrng1$EkW|t8U8bR8=a1@4K*c}r{L$uAwYNP?3gHTcHzcA3UFKmr&O5F7;d!d$~>88 zu{W@p)rRBcJIY7&P#ohg_(-zncV}zc-{i$yT(_k!1`MyYM4_2Z@J%vsZg_`q{O@{k z#;-B~>SyrmA46NaCe9+-=+12VN$aBH*W`MDgHIxvATIt5#EY2%6jXPVHFMyI@j`Na zJ0Z%YzToD@Pm27IRL0=p=q4vl@=(F-yLx`z3fQAJ8M(HnY`it=mQ03U%DU6VaWU;F z_SNsPHuy~GR~{jT^&;9Ys5oElM(4bXDyBN!V~_J1G2K+VhE{ML*dsirq`g{?Za?UF z?#avTMFTnvg(i6_5FiInk**9DsyFw3zL`X_`nr36@O1JhjWra)t6c-a&s#tRYBv-j z4}sKUx6p%Wd`x{f%-jGbn%ubQV z9HSHd@yGtiS%ZAr$<@348flpy0=j$=t?^AgQd~uSjH?blP_whCFZ3~In`?B3EZMf@ zqLVPZ6=(Ci0{o`J*>aPaXm!2P;|Q;})KlNrRDh=y9OR%n?+a0J6CkwcQNME&#GB-v zMR-yKqUhZkQhlXw^rv@P&6+gncQs+Du2x^NIbio4yqj`C0M#nVq*evbPRGf3@&?@-gR{yHHUIO7kaS zSxkn5bs4>O-9Mo8pNg#!LaE}RIIeFK04afB)WO63xDxOhTMw1re3P7KSKe1@2~kjN zkIgDIH!ttoecD|nI@+zNdAo0^TuM^o=Un1mrSvYO8wH;wCBcCKROzMWZ&tz^yZah{JP^e zyxAQu8fH^XTOw#}95M`mBt=3pkL(#Tq~AQqy>GIGy{F*6=7nd6+^6#mW1L+DHIs?i zowQ{2+xE_U30~C0bW}Gk4dA*X_b!y)Ka!}uU!T9ub=*yS@n!G{FKjRUuwfNXjYVK8 zrsuZPsTN`pPA8qjgG+iBU{!+ zr(M!N^q$cq|IE*tQod(@Au|B0T)=r*=Op-*76votl9#Y;;@xka675fz3!i1$apx}0cb!t}ux5%yqMW%hb49&*>* z_b(>+ZRnLE^8*dm;{zxDy(epx5K#VCTdiqqSpXcSrL7*w%D{HJz2zHqm--<^tmf-P zNZ`fj#TqB%AQ*QsU~tY_YInezvwf=nJ*Rj3@936qOCi}V-KnzX3RQ_Yc-DN zea5g+uIz$E`J2&v+>F}96$-c6!;L_zt2TTy6sJWVEf3srDX4ShsCvhXU5~qH&FTkAPOXm&Cp4&cLl3~Dh+fi0ur^EJGlh}a6XNZ8 z?V;c!Pgj+KRJS^%w)#Y^T6LVMeEjnQNeU~ppA645=KL}nt>KutXgj23$D_xWPW;2B z=F0Zl{)6t21#d2qPm|HVXdjTY%h7{D#q@e5Bak^Aa6N0MsP2zFUMyZ#;3T-{&dLX} zy3)RE$r|$l*L8lWS0PW#b75I8&pYCTok*sk4kvIn`sYQzQ7Mdy-QGS>Yo|n^usuqR zxqKA*S(*6;=F>IXa(Bg?m#&yZ*r&3Zv!Gu&UXsrfAB~eh<-EWQJvP8^QtpXoatBhV zAmb`{=YYR*vSSDvoV{gu?Qxv8zx%Mv+|Lm7t%IY#t~lqbfcWir-|UaNQ@q{lC zB3?$Wi;VvqxNihi{$nmOi8Aczst@^Du0bojZ_Go98J8P%-`6OOYLefG&_pD67Q*wEThFH%&#tuG5t)Z5F%HK={1LjMzHK}EYy%B z=S{dwp^y6Q-1z$h0wshQBHowv1=IQ&icgbrPXbS+mz{H0bK9+-{mqEm(&cj*J${tF z#$Vk%k@qF?)$S1QNGD;VkxK3@5(6hjowp2ioRP8LflyNtdV4cqk0rhXwWCDt}2!u~7g4kKivK=Jt9QfxDcWCk+B}wC4kufp8B%jXz zXxW&O>uzPYK@Gm9mFFJbvhpCdq4}1@%@OcjqJFO2Z5;!#-SNQbJoeB1P}q&gSO6;+!IUF{lXM zT~35_48i+vuu)n_iWm{T5zxFmGZ*OMOQyhu&ShEe{j{)^SDk@BbZjY&SpyHC;+`E+ zmchglL4n%qQeX{P`;u~4H#*`ZW~UwwS3rDY8DY^x3})XQlS&pt&10(-cx77l@1$P{ z5lT%6b10VUw7IIRj1q6#)}rDRe>#YPYUU(jKY=;T|gq_;29ET zyr?(*slLA7tf{2+;8xyrdAH`7jB!}R1}L!fEgQVIdK^)_l;Cqd$dvthO0QjFg;1v- zU{e3|@SDqKo^cQtMw?m1zO5b8|4*9C*>6EHqD1wX<_D^(-z`F>=IpPx2@aCJ49QUn zqBazqhAEBiQTatsH9n(~5wPS)xsfVFoSFM8D+Q|50JTN5=QEN|KV=YbLrkU}vO;z^ zXN=$8SyqsQ4EzT8h(qn!sVq01`jGM0qe#WVz=gO^VN9?Z5?^}coI27GzOy+l&bct; zDW))xmgd$S`NTZUf2#O!?A`Ke&w9MDo7;uG1v)jOZxqf1Ic7J@-3XNI%|h^QO4_R7 zuaughbk+GRH!o^Vh6qX7S!on0{t&);6cWiJjJn}-^-tw60>-=unJ+IZ=pytkC9{3A zvFM64bxxZxSRZlOBMn=P?3f?d_94N}Ns512fPu zZtK2kYk$S0*?832=1`HLG9pYMWhR2z0r!BkTB6@3{Mlsp867v^>u=(o+B09)*V#`& z<+ZtQrD;)S{!DWi<846j%%t%-ROeKo_%ZqiY`s+qQ< z>YYi(nMgLW57%l)6vYUv(F(+_yp&+#7gVcG-7l131YT0NNgks|;Kpj!npYD*DM_wP zvc^5TJAa+^QAo6!_U6$vHgET?$i<+``58T#1Kp;B(XD2A@l?T(qn~|hzC_T_#!0)jdqS@8jeevq$xp%8v0*BKv&Mhc3cTZYVvWxV zZ1NQ)(2GR>DbZ5TWGK8W@(`<2LgZD0j7cyYo)$VexIgR}j zLKOfvjKH#GKG@7!Z7kUe_CQ>YT&M%=%53cf{hQn`9>%OL@KpU zajz39$wIzUgEtr3Q@R$mtt|qU#_Vl77^Ol_G z^=9m-XN(xVZ_y5rsLz~QOjO@)>n6}VGejq!L9(={Jq0tRw!TUiS}C(RH-ylu;Y2>> zQi9!llznc=cSsqj6Tn?7S4K;K6y#=`(-;{3L#wbWLpPKn%&8|MCthnaqY&}jPh}b zW9_j6_#NYByImwYXadUlp+P#ghy|b<%Rnn|l&g{mq6`!~hdiBAB=%Ox?onQGp?ep#$_CBA5XZw_n`3JqGg)KNy?gMcm9Uy?o z-zsdjJYP|G&Qe6R-E(nmLv`LQ=P!|7Dloc|wmZ`(DHF3|INAU5PI-wLkh1H41 zUAirFM2)`=?m9uX;ml;I_@wS-#(u1-?7R0#8?euRZOYS^H8{`O?@&wRSCg&l^(SPP z67M5?X>YX2i)K9wu4fZPS@%r5Gg#ZoV<+IZrLWN!vk?js{;5{g;%O)!a{}R1%qhRX zkZMM|WEJ3l7U*TL=Dr=!&d;H+w<-NhZ;R+}A?^)OCurHwjOMB!GkgHNvVh5v5*9w; zpq)Ru0e33(AlXu0bs>IuPDUUvf@9{A$yF z^)W9afLdDVj+qRJO>VZwJ2v&68yNijcqB5Z6C5wikNNacV+x|VrS;w(iJ53nd;O9s z|M(}}J(Gpm;`}oj-tE$$-Jd6N?~$ji#c|b!?hDO?R4Gsde-! z{I@onMum>IXoSqD^u*;hs&C#UlOs}HJHg)J=bce9Z0g)cRb&1nN420T5vHnWP(>4B zKKeTEFdKUogRsZbdEu9=z%o3#;!0y(C-4#!F`3{lyf1aCWq}S_#~0)j;Q@4S{A+## z2p;QWsGyTUg&-%-iGvii-^d8{PF;MuBA~M_t}hHWo;wNq9<N5jxHoB_Gh>XS%q*?9Zc}z3B3&Bcv%n)8D9F_pE*YQ7>Ffau7k-JCKzSgY zL%TI(Mcj>=!#Or(I*GzDA@4m7U@i*XsJc|OyB(mCnp!}5qahRf0jf5aaeki_*iPqs zsL;@r6xu4HXGHh?f*%4^0SvpgvpqBCj*lJQ1t360RQ=rR1}~NUTKXo5ju<0U}KV>^Qsum4e*d-K<~;FCplx7o;gh zO4^>th>}m9XuxGEw01tdR#IDkU$Cl@b~Nq>4ncJNi5B-I7}!m|eLlDZ@PINCJEBDR z;pIY2M1AeQ2YDgN(~z{=j;HzwI5QCiJ2xjF4h^x!Suy(`8toRRmGI5KNGayFzc@J{ zgGn(b;KR8idu)YnWQ=9?mh2E=Ij-qVD!#`SW$l7!V9C{|0vn40i>o5)+O8|F(p1#? z+XsMthGlUDuFf_xfN+b_^WUz482(kMuUR6bi7I!k(2|rSCs-JY?h@{J9&&1`5(jaR zacD#>50ByHCe9DWv9}ahSlOb?a6o1U zI;wb9S@H27=`(Tl{s{%4vg1P)FF#JC{*rlOBZJ&^`#_dp57x;A2-qCSH2w-d?RuG4 z{p_x!*Waf=X{`f2hY`vKtSw@|a$m+tX^GQMG&m*HPW&+&Va7Wy;6NDNXTg|6jh7Rd zt%43dI&xJEo4bO>bBarcHj9pMQ!Icz)XyltIZ53zOs zefXklSYC&J24K~h&`CO(L3=Q+^k-OLSR=F@ncj!fl~SFzVDF)r{65uw+UZGQ!!2$# z+G73MkO>*qSzmf9ys@D8dk);dwV1~@H$G?iXlH#nFmm?S-v-Nc5->jm?f)Q9V%}!B zFUo1c=M3IvejCo3xg5=4%?w5285u9|HQQ*M(o(az#i{?o!ze`+UEsT?B>+*j<1ZAQ-4ih-Z|849iDIR zmo&ZvytQAN4STa)zy0dGWWlDp} zP<>J71BAzQIz##)i6jD7`R?s=ypXC2y)e0q0guMOK-dYixMc2;NYg`F=mO-|8%!&; zp64qd@>t8{$Nl#LdZxRbqm-o8OD=<9(l;P)h%mJ5uahJxh`5mzg5Jid;Pkx>JVj;* zI3@-W9}$Tsiz_eIZLeLP)*wrqsMlzkc#K+`7e88|lfKPl?~c@6MlcozQsC2ktvB>y zdCK*6+Pu0*KF!zhuxy#zN(EZy6=b5BjoWNh(#UgL@VG;y#m{~(ASrQFA2HIFIy@PS zMv2-+v9(`h-8GnyQTl_0#yoA!J7T)P&{~eY*cU}m6Ltg_2La)!-sQs|pm_Cbo*V+i zpFEq%9k+Btp;W!m1FB)92bJ;C@%BzS9!*QAoIH6>1`sf6Oynf%Csvy_?NRbTI|?}I z|8lE4d0iN-_mV74Mvm-T-t&qOr<1;ZQ#(lHaoZQk{#L`hJdpP%gCbQ>unog-RYlxu zw;T`u=?{=_D4TD_84+`yn;C!Q*Ih$1(){ViYU|9RwSN*A%-r!^<;TD;_;!|jh6!ERhvv_JGoLK#?^?n3z){n z3Ttdzyws+5WaX)|hAv8QKAM@i=YuG5y!s~WsN1m)8jtDA?5y~g3b)1Xv~0;fgBqKw zd?efI`mE8@J9are`ayk0O+YKgeG4|@;FrmkwjD}1C= ziU|Yr;QIXL_!yh}gGrCa3U#S*RH|CciGqzMa8uVv_A*O$o}5rtRQ*{UPB0r!{N1xw zlm#4e7voRi-_&JEX|}T-us_SR{SynY!cJMiPFpfVq8h?dlG_^aVZA_aFX!|GO|+2C zUc(pdZp9?n3yx7WMN-@bl}xw_#Lf`qc#cX-bq-3y(i*!-USu@wm7tyx0ToZ{o4st` z+34~sR=GI-kv<;jxdxO?yCXJ*VkzRvHaN+|ORnMN3XHtEcSiIxFDE#x=d(M#a2ec6 zja26Qe8)rg+iAtL%DOigE|3>sr?EW>AyH|i{>lNPpyGkHQJ{J0LIL6;*RX9k4P{9; z#W^_{1|qs|o`p9su!N31)fi}N4tX~(oP=w4@8Y%Mb|Mch*zG2QEunMQi<37VVm(eb5Uvoz|sLo z$6Fdi9~%}6as-$=qbwiP4i9$%3Ew7VpXRShmc1a24T*S5XSQZiGc!t0Kgin;`mHHe zo<&a~8gxk^UG)Cz~@`oKbE*CGhKx<|YvHAf?lNY^nCf*L@|?8!Z@G_9RqXoXl2}v(}PK zK^jNt%5r<7Ia+fi98R1Foz5dY5(nNEBr+?KI`AlI!dN=rEJ(2hIH^6MX^*1WpKnPD1CwzOCyfuTnaU|Dp{L^?H;#rSPub6z*F9VB zHt>>lyjQ-2RnK9&Y;fL*N?(9B>NY%bE<-K|2$XmIa+JGe#|Z4(3|`ItfG=L+ksO$b zZ{hJ_gmd(uYzt7#L-6fR8I#r@QRd}Ye+#sKxQ8DPg$^-zqeZk^rs(g+Vs*Pe4*zKF z8HrZ_E2%gyEg7HQvAG|RYs^X!S4%pJ7%9HBhh6vk=q$i)0rw>G^^N<@?L~?5YJC#rNz(8OMR80BIk4kP?AM<4f_T2 z?Y+>s+Y%5AbY!2_JOT=Q=pPO!nQG&U^~0h`C)9}sr{)eFRa&1OXxuq z;c)@kEM5s^3My@uENKbUaYLyylBd<=CRz30+aDIL2UR>$(ghj-2dv@Vf>G~jO|1)*%n zu(BU)o2%|pXfa?Cm6@>ZmI!_O$%~nJE$bIrx$bv}iNxiRC{MRMe0jc!_7%3?h zH0zBXic(n4{#1qu3DM#(qyCh&R9BTGZw#?Fr!VKV50Cq25!f3A7Lqe&%!4PAud9S8 zNt(?L95LQAw6P#`k;<~>X=Vor|X!x=}5!a8hRAF z-1Ts8;vPGN^avylR7UfPtG|gur~O(6-kVVdtyBBhf;8IChtg)iE)h!BcMmX9k*`|A zw!UBeqd+LcS5SiiE%FnMAk84h#(t8bC#Wa)lW}7tC;E2TfNfihx0QNuV_oxz07c;Ao$V&i*+@u?a;mM30U0yX!q(SH?0KufLstMz|A zs-gUAh5r65l=RPY{&l@ShNOq#KR<dI{?i+1lzBQoQBpa5k0og`EDMZs(uw|g1{nSzCAj|#GvtTlzn1>*t9rw6zZ3oa zvX-${`cIi<-2-OzsD8?j(KvrfvNaMZSZo9Ch+1sLa;jAUF{u?7$r=igaJFEX6r-}KyeRUN#QFc zMJrA|eHyH~*2lRtluSixtr6L2(h&mDvr8d&^(fL;Yf%r17fM|kAJVzP@v-6#ZiJ_D z>V$+j4}wJa?&y-ZF_N3L#;w4oH?3y3XOa10fnkUsp;Gmj}}9uw|T|UPSWJ zxIbOMbjt|~ji`M`IoL-KnZOKR%^}GvL{?dW#RlB`n24eS)CK;%$RWA9%f~bMbdN_+ zWHQp9a_Z{p_}I8iIB-A!Go>iqWJE1=Ee&DJqdHVgrq`%XcMI#GTbbQ#pT?_!X^)3My3s%YE zQS)FK<{pY1G*(lPN({I*$BZLv*BVs0#US8oijJZa4&V7EqnCyI+T?N7HGFd<1L-Y` zEc1>^#Xtg4Xf0RQmXSi>m5#$C%207vCq*D%`>uqYCb-q~_awkDD1x*?U8Yl~!|HKD zx!-+l%`t6I(}f1HtP2tHzlkr}qnYl8H*hO1@3{l<^GUv9n%BrV?vg)FnX!9+YJg%Y zDpeyUI}XjPk0aq^J0n=(pWY`F<}^i>AYW|>En2o&AWIJTM zV}&1`7amWm4YGXOz*9z!zXSh;KT0C<$;GUefTAET52~8<%iO#LHHDibhfJL!{F$<4 z@5-14N-BG1E0A-J0QWA@{<8W54Rja(p#_i0Q~3AXu-@3KVBMq z;e;fZ&(l9p$HU9-VU9GDof#ZY(SoAE)hBnU%km264r#q`Ps3+X>(AYZ4Nh|lu~Z5X zr#R@2MHskQ@C!x4^VYyh|Cy&BtMS{*y76hkdM~7`fb-uK50*eh)b?YkOW>tL!s>}+ zI#;GY9l^z|;!vp6x$A{v6drlcg6S|m98(g(pPpS;BL}7=Cig{BCwv=cHRzVw&boxm zt9kHrTg)e$Aw?DmC8FT$57zcCtpajj;u@)$c|*(`5Rc!qz1;p4BJ~xs{#(u6H!AW| z`h>avB|-}N=h zIr%bxu{o|S%HclijHiT}aPm#42+q_a+1q%>bYuEV1wAAt*Rn!jfXq|$?2L_uwsN# zw7<@wzg9~S1dH|*HO1`aeaF(uB3gzp6&sZc1&miv9dJkSe0`e5+G`WXvDkkvbV?bNJc}eyBshnU;1lhMvCjt8EVj7L{h2O_H zOM7q@RazSIvkaeSyK#%7w&fit2e)W*j!H^DQRgtC>VuEAdkS3N(0T!Hs_-$6ajNzX zT&}`I#kp;%I%WDN!B3Gg2OXR28*984UD5`lp_^d;Gr9*sKYf2G5H)Z${O9yfC<&#m z`+#?WqQX=6t*E?Uq%KdJNt}J}%u5~h=$Q!GuVDZ7*FJ0_pqVvrvP?@Qt?<7dEGTl3fqWeFXj&$Sy{--Ejc_{o4K1m<&TX@z#;3yn9Er%_pfHMW z^Tgizt$g%{^>X*=?kg`@m1lZkC+jr@SL!>627gX$^gX?xaPGLi?R8p4|8T=>oJTYu-&(gfLD5@ijo%GBo2$iU6D>E$u{Oc zv!wekZ@yqOr!35thWS~OQ7R)*`cEx_ruo`GSDW zfZ?w8VLw8}In$AG{1ng-)VBa8GV65<^+Y{MBC=rmBQtQ{l}fF}iC`?r@WIl*6Kvyy z3hS-Doqjw-H23&kcU(1Mt37iE&Zje@sgmw_~rN!~<%%6)RDOV!j{4ULXs0KTOW>*2V?IL(9$FQ5NW z$B<6S7B}Trdh?!#r)GAA)wfexjEe)w5j2$9M)J8%iCa{ii|dxFk8d1+_fTF}ihq_m z07jZ|sOsAS&hqUvWtj9!@FfKz1`3%J8N))vpvtd-CiDv6#FxqDM^A;0gR|x5?my?F zd|`bMCHMUW1@Y{(nafSxDRUwV?l>+_7et<0IpzBloMJ*NY974b<-;i{u(V!P9Yz4% z%fh1Q55RVevn!c^q?jWvp$&zue7!g1jKs+20+FtNmW^RMC=#FNRnObX=GMS_Ro>9h z(A&ys+K#=KXHqVblqGzh!Ki`Kq_kPyvEpkm(|9^;;;7{L{z=MieyAdUcsi@}5_lxOlpUd2JboY?ZCjdYX;oh7F7>xJmz1)9I{Wbo(yyUZpZ))`P5$1Xgo=z6 z|Mw~6|GUfp3h1^^g7++jhyK(ZRCpe&VM|qc4Nf^4#8qUgTz4Td3VwT#k1AhwZxJ2B z|M>@N!oo&{o@Lz*o)k=sf@ddA=|F&Nre4yx;d$7f#%4PfL@W>+xc|(jisXN{27djo z)<9&h*m*^}SbAF2Oo?bdSam&==mYF1yz_4wctgpYtdvrF@pOhrRRvu2!1O~ms)gWN z3SL=1w*A498L3f{i(mEh8<;~diCr{~#Yi@<1TTsUZSF337%0ZZISMT{iQi=9HeukH zLhU;zpAq00I9b>&J-jT~h8{6??;Yy1jvc6*58@an1tlA>)67fiF>LLwoX$5abW#%{ z2T+_(I-4$<2-9$ks0nEb=TOsR6l}N2t75j2K~P<0tWrvmT`Xgx zfYFJUDT$iPO@31~{i*UWX);$4j8J`fhQYzy8Na2Kko;TngSTdVnybT?pcB=iD1npu z-+a14Y(QSCEDMnn>fGn4Gjba9pQfWEcjm1rk!ER@(Ci(C5RXClt9gw2X^CUcr6>KE z_vD_D?0i%^_njB2GFn3V%INCMSgAjY$@1_ENeIE!>6mwGRd%>Nz>S5kpA=Jh14~kV zJxvcnwapC6MYI^g#b%mCX;n}Z-#9pDTN9U!piW1D;VLmFWPB!+lb+O_kEks{R3}u& zwoS~;*&!g9J%8S%)A;*u@>8@=BpTndmXD3ywT_Qb?jO@OYRq_cG=bX=cqLI(SkkVw z>1e@nhOa5wd^9W`p7K@?J%VeE@LF@X4+XegrmXnWPoF>$KJ02klDhZM^X-~NZwp|* z2;}8+siArGO0rNBuLD12-JEFaiu&Vqkkrn;z#1owcD8ij4_6@DEFDAk`Ha zL))BPr@F)vC~SC*L_k^3Xt&Rr-Z=^@GOr!6wREjLh-RuPzp8aks6J~iSBH*&k1F?G z7T2_X)srqG@cebbO5O{AX&N*Jd`zm;|F~UP_J^c)tXF3AcE!g~sJSvBdgLizxjiSv z-T2J4_Ur$cT@|wVmsyoNL3*?l%`gEuEw-@($|beTum-;nBdRUMB9^Jk!O9ga1@nWn zODKJ^pNa?hSisiVr7?6rhJ)Z)820LFC0X)uvWIxg=RtUJpKANa7x^fS$=xkpvARj` zEhV*(#^td5nMg*E;4PqzQ@BqdRhhC(!P*jUTfy-y>pQi*?D+R6w)=% zruZ6W@LqS~=T<3%r8GZ6PIhMfwwb>e*GU!5>kV?W{^5w2^J|3AvJ9)RS8Uap!1yq> zokMlw#1)Q5D2&~ zFjlKP3fA=T3xcWYyf)k2YW*SFZvxzx96rjH&59gZWws!R{VgsRp1 ziynfAkC7%p=Jq?8lR7Q`&wKLu5sKrvUAdp8p~rRAuC|HzI~iwoOQq78z1MR5V`>?~ zN(~OH>jTk3o^oz_NuJRGk~|H}du0tf97mrw$b*Up^XEiL+m{N)T*CrEaQodgHY>o4=J7EW9hU+GglTLYAmI?bf712;~{ z8|PA4ojIX(cvlZfqfpbA?^e%nxnyS_>Qz!|-;WV7rkL{6Ycp2rS!?h3Yo-Pw9})}C zJ&G}wBl=o`xr#uJCv1-Q^X3)DVZ<t|qQ&P*>3Tur`nFX0jq1rwr#ZvT1hMS#hU**+;gH4kcC(NcAWg8uMm9Z24O z`|&9}g+pg3@2m}fqvI+*b-zpNN%{JB`1YWy38F@p=92p&W)4G1tu+9tsUvBtuqkCJ zYpQ37O>5Fg-hkl@XT=1}%=tQb5|qwzn2+n>hKG*NpFYsAPK7DRq5J=2YXg=xvZ@2} zj0q060pCGpcsAl06`j5%6nz{c5!LgdC+m5kC5X0_m#+L~>po-9tJl+1BU|OodZ-^~ zO?dLZCX3WbgE|G}Z@4D-9{>*K4mX5js+V$dBoGkP?pI7)_{4M(y5)LWyRusRUOj&2 zEHPCWKpS>Z9jj|poFE{k!`qC;`~{EywcBC1V51P>tU4-v!2)|F&m%XMaN#G1pU%%ar$)Rpzit z;!9cdPfFW+*9Y=GUTh6@eu$P{7I#k#EOsLbGnYLBR*Xz0<$;U5+;MoMe`{PH5uI9z^KGdh$v!rXzQ+n^D)r_|NM}s&LaVX z65_C9{tfcZZMAMd6EEnC8;`G_#i^5}xcCrS2rs8JUeCgXF?duOb~iXB69Z{Z@sA3E ztKBDptJB}UojaV`QBPGa3H_+eyKg_wsdE6+rkciFD)48(s+wiA%H=cD=MJh|P!vXWYorkHE~rz}7HPWMZ(2S||yBV7hAJ z-SgUmi)-$*cj}nCjpaW&&u!-oirAHJPvvOf5k}&2e0v)De@7{R^jiW)`OYP7-U#{D zSOfHXsu912^fci*>y1rltMgv=P$oY<#Fla+Ju3m!S)Zb&VE?_j`(;7Kw?N9S@1;o^SaAhqwZ3oNx{xbF^85N*Nx5ue|Cj2W)3ELw3Q-v z+c-i5(22{Du2am>nPueFuRL{eGa2RY!wA_31s6xrthTE@jOMBWJoEEh00K(!q~5z_ zD{=aKP9yyVnapoKN4DVdHJ-}W8G;&FAM-1?EH6gjDeKmGX9K>JZitTe5L?HDFMnEf zfZyBYcFv!pgK7|H&{exmfO)q<{yOpJ++%9dkzv5WN_Ez`@*1jc)1jpnWDZBZWQO19 zgr}K$Idh?3Sp}EKnGBJlCT23#UCbw)0@=n;ZHU#HxSNQZXc=r8_88IJ53J*e44rEDw1aK@mS&UB&+XZFlf@ z^NUU~VN`4pRc6XIXa2D6ctMlkbM>87O*iHqJpTpw%Ozts6!H1k;IL$~!4|!$QWWPV_CN|p1Y;NFfhsPz6iPSB28*yJ#ad&$y z#1dO(PQG07YUHEAKg>{i+7d$6Jq#xQ3g1Qf!Sj#ltZBcmXjOAeW1>(08a@e8H8?pZ z#wuUy{_P8x7HhK?S@Y64gJA>1aLTGkg*)7P;4?agktbXA&^bdS!!l=Q&L|!l`kQ&k zz@&T3c!Bojw?Jemx*mROPfJ}pUv|l)uVJf57@Z>PzG}$K;wL@+@?j2)S0JrWU;i17 zg7SK)RUvAMh^)BzoPh;f=<3RTzrp&pKh4}?7&NEsa?ZT>t|<77jjJA;NQYwkhYMezd_444IVIfiz^R3t;GzC4=u(sX>S}c zIWB&fO65#u#x`Vk^zek+hsrssiy8lL-ITzn+8C-cRP$WRq2qT3N(=(dX%U!qTwU>OJRGiBOKN3=9Z2%n{iFMVBq7N25agaX}JRJ%0(K}Bd|{oefp39;uY zxDO{s^{a}Jhk*K5lYRNrNFTti5d%^zCds%TIK=UgjQroBU<6o&;llisX0nCLn(!MJ zvL5p)lay(A7R)NgYMtIzCkow;Yw>?s{#x)`@T0C14?*z->x)CIOAVlBNEIi22c*Cn zq>dN~`n+^hg@+p?xnCMj+<6>ok4o5N+Vow!?9N^Qy2qt9R)d|hV{k5GHp5Rg zyH^%&d*$p_wqNo2qCZDZ=nK3ZjVa&GW+EwY4u4YEYUOw>HnyE~gA{E@yyw#1={(C! zNM7g@HhzR(Iy|C~>`XV!hpwfpYV5eX-s|VC1sCne9Ipl>jjw_Dlb{LnFF?_RGTE|e zQAm1Pyyf&%ho*NmGU>s8T9K!2;JC?nI(=U*h~9nGn53>g0>gTqq6y6}Dxow~&fU%Uy1dgKfB$c6S^X|dat8{A@55w|L)gC5UW zmpv*;Gp+IF1H5?HrMk4jjT`U#+3U9BOT_7BYw}UJGs9I?HKS!(rcHQe%MG5+oO~nn z(P5)U%+lO>sMV-WOJZ^qo&IE=Yr!&Vjp}54?a-)l-Kl$x&5NDgpayI+hnOjj$Ckpt zjQ4v0Tb8}uO}#Uco057I_f+}Ta$iF=wejM6X+Pw$jBTQ9T5}c>FqZB3;e<{Gz$#5F z(;LSN?mrVj!IJqHxh=hrDF1dy8TGMea)k}Tc1yfYkC=hpDyVFZ)Q7Rk2uHoorhk!E z5)#f*_07)CN@q?WWiXE{briO=JX?63bCg$A&0RFFNM{g*x9mfXlbTp@`t&PLNVe=n$xoI8Jg)gqOd{r%sV_ zh9{hdht{frsMcDG0?dr@4-R+&^}fZy5HcCX^DbVlePNN7;9D2WG!H4$uR~=sr4$dH z!9)an!%-$0|%&BAL_$?_OpwEKa1cfLm z@9f8Mj6WsEKb*mtVxGeN{iF!Uf%+)e2%*cJuPi9>JBomEH166+1tG2C9|_EF;T|8P zT)1yklZb(6ZqP3fQdrA%#k$PTUY4|4zb0z=V>4jyTFz<)!Dt5N&tH8G1*+0KpjNB? zH4Ap1WZ6mWl2WR>GW*AEO4fe8gs+)kpYxw;+RdPe(7sb*c#B>x>Wg4&zw@nYX`A%N zy(!J&@z1%f6L|4!ovl=s-TO9`|7+?r!I%$I0z!;B$j)vS*>)|l!&s?ABCt?HvshD) zGZJF>u6yIENPo}EE(t=V{e4eH^t_#$~I{IYya$f6ej5(p~Pnvth*K%8-7hT z8M1)tu+lViCtqny>dll^caxJe7}TTf)Wdk(hwT0HFn^&V1JEchwQ7h@NDmwUlQPsG zr@6Em6ro~Jz=-AG32rHX9EW0yWRV_qswmVw%hF!ZH^E|%N(=cP?7j6Kyth?vV}*CFMqy^mZRlGIgI)~$cv6X!K5&vN^jC??1ScT|69h<#}& zMFs{q05IE;WZfaO=?@*7QHbN3;-#}bf>$FAgIjk?W1fG2l`IBw+FX{B$PN5QzY$q(A5o$EePmQe-9TmZ(uH!VUU?{LH`uVVZL{nf!VKPI+ z(rVGgo_kwvBP8O$U9Q{2QSN?5fF`6kobv4-FTC8Qd?|puxP8h zl{PwU;#1Pp)CZ*RR$}XM{z@s|6{MnC0^R~*UxqgZOzC=Ivy1;d4O`s`!pRKYDCpPj z{#v-$n%MURckT3vtIMR4wIRAE7nYsF(2=T$PjZ=04N$(C?!NDrnWA(!DJ)o&UFkBlBOca7q-|c(Jph%+_^zorFkescvk8B1ZG!sg71kjNfgx!mq+D&odniq$a^Iy^KiE?Z4`~H; z)G^D^2Sw$aFBs-Y&HjdO&k|4Fz1pJ{RIO}ume=q3>{>~=*rAz^Z=?0^xIyr_aBB{d z8*oPZjYEuT{XiRA#q(}t*cTACd<~cu5*xuH@cX9P@IE?0=iJZw!i`}lRguoe3QHF@ zho47vNwM7WdKR8&Qn|?`#O>FW{%O%}Oz)PHoSz?na-yl`BKCUnyx(B&%IW9@CJ%LX zHnue<2Mh4U7gRUNMqX{|S}HI9y%b19jUt16Rr(*r6$^D{7p0RMU zLgqPN*L4wWW0w+Am%^87W&5a>VW>{wR`w26Wb3SNfR)C6uS@-DxY)b#UaQDoUR~-p zLF-PbJTmb!-IW7j_btTJ(MkI~Vt3pMUWWf%xwl^s;WvW_gesXE z#OV-08BX8DU(tsA&hM{=H#fHW3(BAoF2Rs%S7K|X1$oF%W4rHZ?uAz)v`i}ajFN2q z02&W*>Un7kzzh8p%^Ppr@;_Z$XyNZO1eyt5T)4TOeR&_YJ5p>DZbVeR`7#>qO#9VW zl5+UXmjIJIr<mp5;B%W$Kock!W*hB^7zM#eFZ7{dw=ujxHzZ?@&YVnK@6!Ls{)ZM;&Ir$ z|JvU~4EeF_L&#t8PXHm3Yj>dcRx~vkp*0Oj6#TLBAt)rOJvF!T2GnR>RG!B)cW=4p zFAh>Na;tfUA8ajMmv6Ne@<@Hp@m*2Sg**vO&n*?($K@R_HNy(LM)*TQ5!$K^&z(9X z54Fa`IuExc7^9&QxkdN9(_>30Nk&P%LC7e!?K4DLql>lj5+<}=d$@86iK}i#I8hk` zNE|vlKYR67|Cyaf4q|pjNdrTf558#l9@$!xF_#`KHpE-`-+1^kLaWz_#0FY+kAbTl4mAm%#I0-p z6LyIp=0))a@u531+@N={dq3_!obvCZ)|`ZOaPE(Hpz+L1uA^bUv5jJ&KnXS?vVtBZ zeHt+T?#iw#-dIO1Pcxx~Gj*sJ?B3-p715X?u-KG^d{i_UT8b&@3M%8k?|vAquXEdB z1MfBiTWll`JkNyOy{}PlB*(1HyC!7Z9#W3Jekyz+@huo(AR;G@M!ur^ z!OMT(<2x4ro3zkRhBFH)jz3t*at}KPs~Iw=x(9R(xjXb%EbV4*LBB%2b`^4)+x>G^ zW~dBSz9;@9 z4Tw6p{Pp%IDUkLKA|^IoPmlTGqwn~sYMW?je)2$-8rf$aMoZCjBofc%E}oyshM3c8&Its7H9>^Oca_598DiOKC0LvtkY3GQ=%Bh5~C_v0~^#xdX% z3BJ{B3KfFSAM3Up*JhrSdXsZroq7&ueE&MK;Pw0Pxg!Uc z(3n+8-qO`KE`J8yOMzLN(^>&;MYEYiVi%iR!qy}~Em!^?Cs7%gfh47m8I@xjURH!s zSMG+pf>xy9b0_}jLwu&#rMlN0?7E60JD6Xuv0TxTwL^-V(XGfB*F)5u=>Z)*MEk(|@%AwEfbn~^1r8ruMSHA*=y=1|+1&!lJJ5=VUBH#KZq_+bf zfv}-C`^;xFL$4ECwRPd)2#XL#+nHq+w@u2n+i){FwEY4m{RXV)o0<0afdOETj2&#| z?g*dGXZCpBR^?tQ!_~Kl(&X=A^KDf85=rTs>`Q|o0WP1y`bR>nsNX^2k1MxZ%hmPy zNu2eS`wOB^P>gfi{st_zcZT#7DzS4J%eKV#6jPf`@O#UC{z)}XEb`ASg}dW^>?kqW z1e*VGXuYi%(%~@ws;zC$b|5-oEZ|p?69y;HoXKDq2)AZSrKWCpb4*nydc{GT35gAl zE1}bWe(7rPEZa;x<#-YJt7Zwpk0`uUz@!%UWak`jH#4aFm50XM;MnP)XGY(!xBSM8 zo}pjKbkWmlKEYQP6Se=R>;nJ>FtvmR2jAXj;S4qg9%!|Vtu@X>8<>rH!*NdD!6?%Q z3}t#V`sqN)n4>LW7mQ_lCoTU8-(Q>HjF?gFozILXImWf`>lu}fzy(`8X*NJ}nAOAG zr?v8O_jxbs7p0`qD097G){mPwX~!+%=Av55!WrulL(!z-w5XD2&H_h`QRA2NJn2sM zAD8?%7nG!QCkJOF{WZeR|H(|#?x!3M^j7D4x2czUg~`*Nh)hrE6qCN*y+EuGzq4=# zeEObxHLEz-VZd+$3~PzxwQ^ORb=zJmIMY9Pm@jR)`?8yi;U5V+T)3CpJZlvevAPI5k_{b4VG5$i^E%azre(q=-)E2ceg}d_VPv)JV z%yUL?aRU`NB&wB2qc>F}{%UHs-1p9V8M;zGyKzU*5nwNco}l`Klwa!`w9H;{yaiKv z-Zj#!OStp)3|=mNica|olQ=>zPA}ZV)W%yxA;29bq*keX`ewO@QOyPy_~*@l;>gR( zlMnTA=_{o0kol~yuUFqz!b_T(qIT8X9yHXd*coO}hReSGxwg%2X%TlddK)0a_~xiI zuYYUbR&IjpNUt%C05$$;+ri*RYhEx{iFO}6Hr5hqjjYF5$m)t{X~0z^E52(hcjbhT2nH4kkfZx!lozK3O+?$_TDsvSy$7PG=BkB;@O%mn z-@bDoW5!*3(!ltdY4M*VU#GzK7oz|Ak3NNdeRytWBOO00<5!^wPNJi~G$TkV(hyZM z_JKlZt+#04y||;96uN;G>QU9TeIkytbcb$z^j(vuq<$op|36u-*`6lkJ*RH7obaKk zw^tn)eISLEV(C!KY8vl9=#ZdR(bgOqoyQZn@pwk|s?<<^?TR&CD4hpFTVXl`7$8pM z1~n1dsoy2S?f+=~{0~AD(p8D2XgZ-?bN$@`wGIa4w6wN>5%Rz9g z{dvKu%b$)J`(qd_^=of5sB^A75Dh*5b!1Z?Y{bqAJM)R#P2`Ou5fBq+4o8cErl*;b z-~0o4eM~9+Z&o({Gfo9+)%~|a`oA%i#?P$(+i|Y{!8_;w9WeHPfAHTOhW>xV8~^XJ z{vWoijj6Bh|EUG|uiy3m2bB4L-1R_QXEZih1`MXj>X05qME($;&=}nGg~p*QTp8GR zvzBC@y(7ukTb5lmhQv|NalcMP(}JExP0Doo+BpziD?-%2@wBf@IqeT4`!h-YBdfs2 zge5xeFCy^?8tQwuEVc2DT67ySiy=7vLCPOL6gI2ilAtnONvSD}=^08Ie4A9!`oNP^ zx*1hv)FZ!D`RX;li-*uhwwMER)S8A5AO8F{Hn)U)e<0x}jz)4#4rPS~;n>?C{Dlb+ zya4t%LNH(m0dnT99t(}8gnWY)TDTQvrS_M)liUdcj#@^y>(PSZ9 zYFvG>R%jx%)^qBL`i58>^i`&()(+iSPSEddjg583zXYfQbQ*!3dPKbv)sEctkGTQc zi0Z(X91La-uyyPl^Y!J~=f3iAA+b!+LoTW$bH5;ek*f!#v+wu!axtdLHQWp+mV@DQ zZcB;V7((r?Ru^(!x2@zmH?L81s|)O83NQy;s)lux(cYMiZbm^u+k7mP(Fh;0 zlSYCDVo^&hcr%|w`RF5)PYd3cynSN^kAhg_1=Koh<{q}FIc3*=sjAo+&{^V}K$>>f zeo`HEU&xAo`>T%<>vs$}vNPKL4Lrk3W5c?6^%{p1&zJ}}oY6_nv6r z1l$0}>g1RgRA4a%iRK-_uE~&nDO!eOIg!Vwz9+r$0UX0WjDs(ZaD%GPI#g!i2ZaI^ z!SDsv{hQH9iX^*|UP;MZ41g8>u;(y)gk}|`OJ9I*kvXHD5pu|QMn7DJ3<(zk!4O!A z27ulTkZ?m#8sAFD^MXB9?7eCNnmVp=1VT zGI*A3SsGsuiO(I4=JE09+&5H}APa)WlvE-Da&__Rg1gi`$i~Bn-mn=*ko_)Td z$RjxGSkurJj9ZV#?@48!AX_+)ThW0zuHLWLKmv6W?pCx_qFVGnKm0{NwO7Pq2>E|t zXEJ7c1OLW$L^RHCMkv$u1N$~@@(ObNL~qfo7gR%wQ&;E`A{2mmq@B9TPeC-6Nb;5p zT6(;`2&Y-B$fJkpFm zd6z$p%$dfn4`~m55h?pI5FY*)rq^+ml_{JSnwZaBRSHUc@0(W_KKRyRC>eH1$?J~w z_t>@rJZZ-;G639;H;uIhvaApdL{rjxtBa*xW%j;x%=KJ!>bsG$9$y=qlWm;r?{mIA z!1=N8lF|7Np|Ref{;jXTk1RyVXrya*hy>gXw9MYtCS8QBbQVaqYA4+Alr?arrd2*& z6H%K@B9t%LgYqz6Pnh^LeLcILb~Qk@@_aWM@rY_e^Xb1Sh zm>KV8LW}Z4!@>a8OYSB2o|NxIVmbRmO&J$uLqln1%#py{wa&Z;>>te}LyKMY^%RM! zpanluO=&qlV$+U|%}JPKoWrQQCeY!d{CRvJn$( zA2if}19WI>2{>jO7Q=s4TfI{d=uzC_1ACnPs!5Hh*EMGaHtoISJ0Uo^nfW%g^h220 z*4b#kqSaCRB9K|9dR^74soIe+R_Ec6eu-DS(aNXEj?J*CyZ=vS*(Rhd@?JPhMG z4m_;<@=Y6x0;r#oU#;Z`$Lp+`RX~{7_$xf3b5kTKn{wT*74U()5KDLA6*B z;|YCLR=@2bdaL$rM5D<{lH*DV?Oq%^C5e3v*lmtBY}~?Bt1KC!p?;C6Zv1OpN2!RI zMG_ISM2&Lf>e}@MRC*jPU>j<+Up-WRqdH>qsWh{@=1rzUHz&@B6-~dd5>C_^W@c~i zHuQ5siy##ZJ=TkntE=6|G#?+5i$9w0S3f3DT?oJYx%1b=KFoX3<9p{GCG<}G-h7CJ zDEjo*x9=6vHjjdEYzb1%jp~MpSF0x7Wx1l*TvT#LX@X0M%eHZw@08ckBbt0l)-N=LvI#xAon4%TK zmb(UJe(V)k(m39EP$i|$8K-mDvxI{*tM4+&-5Nif1j+R$4|?khh4uIp(?&A?hTF;9 ze#*FqEAhEh8{<(5F15dl<3!e_H#-YtZlPE?dp%f>mMJ<#0)USc|E~PdxNlvX2v=CI z+Xxi72Z}8!p_fBJL~o(QW$E2E7L@xOjwTHJ3M<{C;(IoqjHMz+){+C0BP_giMA-bL zCmB^Mobi>>7TVE6@3WFwzcFf)9h+SpFq9l8?+(xQAfQ}N-n=#KAwSsJ)-fwxAClXj z52K4+oyLBg{Gd0QIxq=sE zt*tD-h?df?bmxSj&mQx`xMy0g-h_`S2rq5FPQ??2B^MMY*sIj=K4Zw8$_%)oXwAHT zH**rvtaG;cih>VTHHBbI9T6i-z2Wwviehs47Zlsi{kG(8+g}vsk$D}&4evB7(Ow0o zZwQ8RgQ?xal-Fq}bRC2Q8!hPQ^SSw`UgAZi8`JIfE#A%~QfixLZ_AQOe~WbM8j7Gu zRj{lfmDdoc0jGkLHp+gtciG^3Xf2mm&T3mUf=67*cVd@d^&H!+9g^F(4LJkHdRmp&*VGve&k(9Zq*S zu%S7k~=#$^Oi3tpR6LWZ7J3{1jt zq`AXy4p+(#Bv7UWb${Ga~wUNna=c?)r!zaZHq>dVm`4XW>rBq+#zkU!{ zkx=B8l1h=2`${J~|8+{5A)<4DB%{yOHf>~R4awl+z*nY1^MJqFRg|HohGM}lv{ogI z-tqE#bd8`gD8b9RHA1HuKfe0YC50we zLrjfR*cB`+)=$Q{Ui~3A=ac3XnpUz+3ym+Cf0%oD-KsOSao~t)v7yZ~-%fE^;SA^e zct^-a_P5oP1<-|j&8g{4AW}mzg{VhpeD4}xK0SBpUvlIk;f}fi@{Yn_3%xw_#;M&D zjJD4km+1OS0L-U%SmYg_a^{~4W<5zm`&FrF*Rsrxsb zJCXYAHLll~1ps%f&6t8}FzEwo&5xqul7fmq31R5C!^R9IUwSxMS)-EHLRs35Ub%U) zvL~V$viHTr-HafjmXv+Z{f)fDqz2U?GJN@X!KmKZ;r!cL=N7f;+6e8`Vd98d9@QT$ zVcpZ08hk%G%^A$5WF6vIrRXEK+SmiuAZ8egD4x#*nZ1v*rgvAU(czE5(dsIPU6IIx zC+2+-U+K8TVm~;ZY^;#*Cs42OL>_z)@Lc)es2c1W(4)Fppc6`#-0y13buv{f+)o|> z#_nQaX>(z#&%eEn-UC>Rs>xHvL{Ql!hI6e8H)a=wE8&h$YDl?jE6e^+%LL zBq2Ep5oJlmih|g1-Bz}F=O?|5Rsu#!7lcl2 zjTe>cDh{2d6Yb&5gL1p0q8feHYEdnH#n<8?=NJ0sFYM)?dzP;i=$Ax(rAe#LUv-s5 z9U~~Pk?X{JKhEg52gdf1RPwY#gAhB7AXKF;tr+n>?TX)C@Cdq%{ZMQaM zWYf|0N03Ou=f(YU9(UH=yVTn<1YvLh>)42H>n|Aj^qW3e`kIBVlpl?3qQSU`bTWqW z;Ubh@XR>WEy)y#PsfRGmHOyTb(sn5pC$Z|eUHIDeoD-MgGJ8`nwOev?Og|K=4D9EJ zSDo%Z_sy?SWd$l<9j)TBgN-g{kd_fLV#%k6&zq;Cj7G_AKbwJv>!+oMd#kK~={RPc zXw}qY469-X_*D~{nX8+b6zULl!(JB^4pn* z&t5gQtIgUAL@dUE;v~iMw+y(?Zm8~y)KlB0w(bX3OT4`bBkBBAH;=GtA?=3rwRih} zi8Pm<`Rg*pX1e6`?+CrTqfz4oBdv(&EA|4FkW=3$vwM2hv6EP_0H&k8MvoV2@3aJ}}Z)}tz@X(on7Y-L!q8I?5Ny;Ck-kXzZ6wBRqU0-bK4U6qxhEn3;E z+pXEI3=9A5lUUZFU2oVZxMfk|;`HjHD|>L`KCTnj!?1+e%~mMye=-E@`IOldX!}w+Z5lD=>l&?)QiRRY5PMp*&Hm|gL!p3hBJ_n| za$v8+IjXYz({oU&J+z_^|kLo67BV5t4X`?-OcIeYw1bx?@RnH)ra&);BH;gT6o>va~lrMY8Kz$@`hwda-ff?orvf@ z8jc4>XM9zIH;?Q{>v7G_K315)&RfRCNKnm}y0)L|WB00sT0Ub^KOL>Pbdva_?zM|5 z`#x7DRT8^e{37~a6-hnKIC7qY`^E?N^9(qXWGVTR`{VAOz+FjmUT7%GT>19r6ACaf z#~dF+QHpmYsCK_9w?XV@%_|BoKpcQ`qLaycEobaLq?Sm5*)p@~;6p0;ws`j8YkIme zO7PhuTnHV2l(CLDVtP^Ze3$@2vrb1d>!rq*qnf7!+wV4VAj(yby*Sq37bHp}7Z(OB zvW9`&8#9dxJg6&QHAUwIC6LYOo_oKQ2%#7O`{0f!PQ(up*!@c$R(0T1&8_p|kxmi` z0BO0%BlzkEQnf!h5rdPu5ssDD^_OOZpI6{|yUi->2^!m1;3{~WR4}?7rU4u6eb%0S zPl9?vG2c~gz`UowBcP&0a1spqS9RD5+027IaW4`!=pbNZ-{ehQaScb=o6!h;!9zI*dK)L{ zI;rw5=lfTPX?ngLwPkq~(>a5)Int1H%8*r?F%5Qpjj;40u$~{;v-FX5sGe-XbwMAy zVM;bl^+~Xt*}P;ozd10|)^?H$0}qv@_&+SqKfVj{9DCq1_5&)RRB}(QiA~oLV1s@1;a48Wzu19zsVy z;?4WbGXe+MkJBe;4{t^QHB;S$t6G}?o;hZ`kM4DNAk?R`oGoI&cG4VL=ibcHdNwE~3|rmEJKlZB(z#XVUg!uNF8x?sf1hjMNekxwZ?S z{;|YDyG5P}W{h_57VVPByZOG9PBa<-srK|u{#T*hOFjl2Q5pX7^V*aOM44T6Fp<6I zn)BMyE!DBj4zzuF3zeEqA#EUy4i>$2Y-==(6j{0LVRg8>n#U-2?wDkbBG6wj>-yp_ zYI{MiBjXDax!`2%y#bRJY46ikbq_9eK-PFC;iTvCqX^pZIoxQn5i&T}Fg!ajz_apc zL}K(KFK4izP4-mtaTSwxbkv!?_0sV@r*jysTax;>zXcCSgX>J|Sphp9k5EiG}6W+(iD`$+%K>AKFKem_g``e^(Xi!=u~J^v2qR zQYG(~TwF17qG=wM(bQ%eD>Sh7N8YKc-|}Vw>Evuzhl9=-S5BDUMl?ykxHD))T?Epg zK``cEhgI5t>+Yl58oAed|m0evr{xDK9jrC5N56YpbLl=^Pe4KNusaJ%g&gp2swKFVnq%w2G z^6}*?3I#E^l2}ipdw2&b#r$-e0YMOF^&3u}<&QF;n9X+WKZ}_%aoOs!lSjasvMM|~ zpME6;JYQ(wS%M0sX`o?kVbez*;EnrA4OY@byz zK&6wM_(0Bpf=#C^PnOF`Uc9rOEmW_Owo_CDxqhic)O_p#`|$9ATParXE)0zMosv~M$z$8R1A80RQ8EFF z5;G-voM5ZfGcVIQ_^-K!NmP3+it>W@5`{)DUUIA0ccE#&U-D}Wj#J)U@$h;gg=Whq zr^%I(nwM@Zw*Ve3`>*E*LpBF2Ea*_CsDaaMRG))6n6I>-M|o>0UgPEQYG|sD3k~qQ z1M9b>xqwbw>N0&M^(i4-LH2h6BtWr8uiX9@7e!!4n3Q(^Fo`rUXPQ5}$d~&vr0VuM zhC^XFP^lI%kV6BnmZs`LO5Y8KY-n_!ta?XB9|=CRWIWWgPa5Cqb+^EQf+)-Blg}8* z5&22GdibA5vrD7;&l>21w$@w6kl;B@J@Ojf?-<6F5y~g*U}v;@-JP>v)yI=Kb`#Qy z%sm0pvF6;L`jNcoNdWYn&y9q?YCoZ9vIh?+OK7rx?wcWdjnO=1Kdb&7w~>#d?@JuX zm$664yGC)!phYYaCF9z(u36>JQc|WyIEn&9U~$ox&Px-sw@(uWEzmc<^LU%1+0f-nB9E$Utwv<<;I0CN|iLDW)~;{;9S_i<;0P6rF9z zf?0;)27~V!A+;%Mw*XrC5k!xFk0QWy)DHkGgJM- zo??RYnVn3%J(we-)OCUVM%}=@&5;rft>Y3${SDCNH zN+e8W(_LA!yqQr5kIQP5r!A{?TR7pl$PrYBbJ!!!H8)+?MB@hCp!Dg~#O>H5`}oL8z#KbP(X6 zj4G1NPbqM0E})q3C91lqj|wAAR4*3LZc)H$bmKG-t*mFQCB4Lc$V@fOH)*qSDMlng z_U(1}~{S`1y?qmEKW-|+R6i(oKO zKbX6l{8iSLCP|1Y5*wX;cvg-!D!~fUWll5j^b&Y`7{#UxI9}IPhGR`^?mq17(hr58 zo97|?3Kqp#atrV1%Idbbut6hQwwqyj1~aAQ3TvS6%8>6T7kh3tf|L4YK2jtjdK1?! zo)dFRhpe)ht~ip-6(x&~U$Bml4W5wsNfv7>S>Fgt2kcsVH^X}RyxSC|Rf*&oeseRu z#{-Z^;Y6D~oM`ufOe@@6SdX9fMZG+;_63&(N)ET@7l*lioYolJtj#5B`b&f-Zm-l} z3r*Pfk`8@wkv^Xq4X? zBas9(pB{LfBCy|VfNWPKk@x-wEZem9w2@+~dv5c;5>p#5+6pM&CN+}^ykA<1Mz$U) z==)*VVeV7le0_EiXEo|Yi5;qU-BGSBRB#NwwDwNPk8b17+K6gQU0^%xBDGyR;Qx7E zXIEw8JHS`*Fv35-2*+Bt11>dpC}2&xK%BjCWlb zKK&%L3TC(zoS%D%jRAh^eY00x@`?{@%jk?RW4;XN2OLDSB1V6f;L9CU6BKRT|eZw zw=YQo$TtQp-r?!cZZU^5m>^uFO!a*6OqKp;&PJ{ zGU4ZGeK4%wRNqT%5a`=KHv9zOapne^vLKvyF#tI9BG(JRnHOKLN>rD|!arW7I%C&w z-S|}o#d=VE*C(ma%^T+lGHd-XLPE8|d9nBIz^S+z2(}8g3MjOXYj{5o`*G~=EK*Y> zWOO~Z6k$-aeKu8XKS;!9iwubWHMA+kkRNHX-JqH$<-^Gk>0ahs`RwiLdhmJ9wwdo> zaVQ;3O@5;D!9Blm%V}q8vEA=rHmG7HQjY8hd`QeefZILg0$)8%YsLI((o&-zsx=eP zA*d}S71T3^{nt=)Jrlx?A|LB1`v)NyI>79?%!z~9llP)|xE5~~@^0TT5>W;JEWo~Y zdd0@A6O?nd@kD4MOXqx{V%l*TPkjy3m(e91&$JShq!FCAK`j8Uy88NYBKb$=A!m}E zVJquIdJmP^eZk(wxXfx<5-&?I7NEr_{-qa9HfpVv8aZX8kB!3@JEE32@I)4WxIhp! z36mKhgXcFle#S`u9oj7|U9!^RA8hc15q)?T-3$1-d1kNAuVBS9k$Bh<##(4YS^ZJDT9TKmnlxcIQHZl>-&j&<2`y0=zyoY0!}rAFAh zcMd@TvM4WNjXr-?Qjq0}!)zA^qP$Q3`NBa3e%{1Z7H$g6Xar?|7!W!D?%v0(zW$s( zAFMk1&RglZAb;n-@ry4}ATl}G3Y{jcZ#*AjmF_2XM-`7tg{JKcPU2(s!00fIuCr)V z41f{C%0jiWRK}gq6(!4RZ7uoDNVpZGc8E0S#rpU%qwS8Gz*OVtXyCu0Ok;u$7e z8*eQ{(SO;lBsKM1VTRtBwEs}tq{0jgAVPrT(}SMB*}u!v3iDAL9nV>qlD`M42-LVs z6J{3XW|-4da(UPZcn}#0wF)Lg%pJjOJma37#HJLSNc5x}hvaA1Mae%V;Vg?3l@xDI z3X#|b#~Vw^&V6@PHP&!1kR@k&`$XMh8&-_MuYLTtFD;a*?Iio`4KuF3!rmO#Vu-@V z_IMlrVhZYQ$-eEpf;?|9b1TPRB>UHWDOfig9s4xKc6#tm&462LJ5BxBTS{DI#T^l& zeU7pWYVsMoYVWnMFMot?+t5o)f4iK^OY2eqXx8j}f(ZoaqT*GM*y*=yKqMp~udin% zrukKt=l2J=k`Cl=Y4VF=`Rj?e&->oaNwP}ea!D-y($)Pl@4hrl(M+noZp5JTy0VO4 zvzj-UdkGlyW)-0)^o?zgo#6AdhC>5pEbGsoZ-UeJkTQ#gBSrkyvpx3{7Xt|5}p`z(&e{5MFk$L;dSW!tSP!FY*E>uKDTOp%! zK8_@ty6|RpT6Qj639X?2d4M0!WS16$>i{9}?L|iPHkLPFO2$WN=k07cfoAm=8=?b- zv-b&QT+tbwp4I5i_NWyG8t08{6pOt=-thfw$8z{~w!R+BXgBq-$%48O2Z^Yy%I=HL zBUyJ^D%8n46FXjPzWlxQ$Yp!;Gy%D4SOhSKupJb(%G!h-rrj~Ixwl`w z%mSiooF0L(oLk>BngM0UF!3mc+|*17g>J5-)R(J3D8fnJ^%&UN7O9HQMFK!iVTOQZ zAh-o%jq?r-6RwXHokJ9atG7H~xIxx>@Kga$+gG{{){L3K==lzdNmL%1O(B5p>CG$A zC>3U=sE&IP2PuYg4A<(eiKLEi$WUvuZ4wxP_R`jYz{C~re(&_=^SmW3z0q2`uWB3{ zZF9IZ4O@dYQk%4cN3AxMp5kFQ4ZE;I^c_l01%Yi!!kF1*QoqU38H6uiaXnf3X|`Wn zzdxU%osXDi!%DW(jETx}n z%ljz2Ykmo4_!Jf*J4RBmusI+u3cXT8!XSmYN2Y?)BU>Nkf~VycmY_NnrnJ{E@zQII zwPCaSs$Xq)$b(W2UUl)z2nqisZ0%~hm8N)I@F2=Hy!{e%e)Vkf$Y2O4ZD?#?i;_8h zKR- zZJFWKd)GV%110jffnnw4Vg1}l(WEx#Fn!1q??PPJs&6m;Uxg|ozSVusm3~guo8PY5 z-(GQq?80=1+F>c5OoC^vu<^G?womDmPtG2TmiKKYXup+s*rNv8cI<7o!P|QExz{Z+ z^;7ZH8?T~LZ?~$TH&4GjRVZ{TBNLzYyk7_F52}kdXIRPLbXg-gs(n1CYE8XoNd5tS zA0jqId|&HU9H_@`WXEmu`FWUm2*Cr6!Z>^dyRD__o(G9Yp^DX2B!sVO8s=Q-Br*pJ z@HDq?C(UJuaJR4p%fp}0~9dx16q`5Z9n^(o?QSFzGcd|r&Rd52dWLrKL2=aEx< z$jS44L&;iP$Dc!+iqkxw?iEP*cOMo}W1`-QIBjS(Vf&9bzJ-4Av=vpi9${e(OE!4U zybP-MWF_tH&*1Z?Hd_?hO`ZNBKECvH*(j18*BRe)@)`=xqfPIf47%|$G#rT<6|(J8 zZ^;zDBfgO(;r9nU8NUE5Fq&oqV}m1C$!ABmud%98(2o=Q1`{ZQnJkba#)q1{@YEL$ zh~sQtUVGWF1`hz|AXwm& zu?3}64+jq`dNJ}JzQ@SuxH7YmilfM)lXPe6G-vWjK!gCeArcN2cs|o_{}FNAK5Ls1 zR{Zn%dQ-UP!5Z1nzK7#6)s@>*71b>Nn?_?NnHepo{n-3!&$LXr*0t}n$Zau6#{J3* zJV4sgQxEOZ(VungT_!z}PM3kQ;F#=N zs%G^#XDTE;plqA8zVcz56ON%6G&4<$I6Jh&0S1eMEr^eMQ^(6Si=P_9v)VA6R@*r% zrDSmACvU!QylV-;^;re#D;&o8?0c&bpw4F#a!YpTDhp&J5+A=twv!t*42%Z zD)2dxCWB$ONU-mwJ-6G64s`7gdW96H2pdIQbBgkyb;RBgKi1JkOUu)T+_sdje)Lg- z)gaE_!@c)USA5C`g5rgog1=ACwBkm^`99;c;a{jQpW8MDme;kGagJ}T~RYKkrW718^cEC(Etc0t)O@qX+sx1{B9AYW~%ZiZassa#+4d= zz5j~jsn#jBdp*f@0|hwqU)X!c=*qgTTeMQCRBXFqn-$x(ZQHh;RBYR}or>*b$F_D( zp69#oJ@36g@4wsH**{m?YtFI89BX2%-uqBe+%o$`IvlbI^H6lC1TO>8IcFrvLdA+} zJp53$GG#y6ar4teMOje+zF3Tkf?+q+uEEw31VDk={i{_H-QH>e^?Gd!@!c#RR&f#n zubnD*T}H{V-}2Ymmri>j$VPJI##XN?S&0*Fs!Gyb8I6C%mp)H@O=WQ>9^u8Tsi%%R z<(<2};s|Be>w8xv23ycNICd>3+1*B08cucT2(kq!0V`GqCYN%kTLra$1|yvu(6*Vu zr2ogTRO&ZrT8VP(vPZY@s3=krzy``(6}D?Vi(c{CXs9Jnrt=WXMwTHllO?T0)KpPX zQB*k*U22zjrAnw7s9t`O5gwL3^H^`BiNl+X6iPvxg};@2TB8L^MzADdRQhLND4Do$ z(0LQPDlftNVK%DG>nEKqV9(B)2mpaS#`@Wzph!|vHOB30AHb7zoBXqW(vK}_@eKU3 z@e5Dd{r4l-^N#;E|>jii4G@oj7pqDWKioW8TB;9+YymFgmMc%H#^G_qv% z)d$+}jI{0WS10$kb^5PV@QcP_Q)!6dBncgrkOVbBQB+agv01O@Iz&CYVznyjo4yh{ z@U*6fkF0M_N|b2@3DZ9fFC!Fqe2(Zkn+syox!PoF-)~Ia-;=dbR2tjhmmzt44FBm_ zJJaezyP;&?QJ>y`wdHM@)z;iMt>{Vy!o#-*%gs>whcn;Gu?OH`eqlU;D0EdG&_Dkj zV~Uu443;ahdcA~*j_1W)FH%HIPf3fT=uJ|g zVr?L-JhR1a31x;G|RfW(oz)=WWa_^T?1Oau@!t64_BWxoyJ-n%^t3+JC`Qh zdemI~KK&`XNZ@nZW9e?JTV#7)HFLIOmT&>os~j6Dd1>oxE}<({OH1>3vHOIICaE0l zN}Z1Hs)aTDem6YwqSD5dJ`)kC_0b9(b@6F*pyRZ7U(|Rs3n5#p&FD%(zuX(~<#y`B zlVJ(YaKEp4@$wjNA+|D-!YNSL!@xt1Da%W6zN$GK@jUgo-&(&j9GK|t`Har7<&kN_ zhG0pIOYKc9WKST1T~Nh4Om1Pj)=+zr3e{@16J4hlwTkn93{(6qkpz<8c9o# zSSrDUt3=k6sJ%eOpd~p}PRvw_7>E?cC*ui5VQMKK9$fNBsDD;7+QqmdOxKwglB(5W z^s! z%=5jBQNpP*KbEeMF_QY96JyVkmey1mz0F!!r0b8LW54JZW1hm_B%(Lh737qY{mf*@ z+zm7oYgP|~u^yg3$m=YRcptxHUF?BiQ0HnfqaF~7hvVEhy{a4K0_@Qm5k}P%raims_YL@icZ9{2!$)PkqCLQ=nZwX`S zmJqnRB)1X$fJ0k$4`zZ+z(^Xya3ilKxT&N&2ka6XI8%G_Vvz|9XY*nHs(*$wm-9qR zNXQSkQ_MN1Eq3r^NCxGJ;lP@CQ(+Gt%md;l1VapeJGJv}&wl5r2z%YLSIyE} zE=C6B_^Em#&qK@Yf!JbId@4tP%}ZCo+$JmE>b>pv1hiX*d*=b`UF1_<k;|EMTT zMoso%2w*tw`Z$H#4?aYk)b+wTa(15dVMr2@}n@asO9Rcorj->sF9DX1LWy@44G_Dof~KJ!?@xkv>q zFH;XDw*j@sdgtDrDCDmk9&c6^M*WyOkgYJ;r?(sSO0m@zoCQ@H;gI-cTyykgm~an< zZ^(kRyM%X&@H_GJj_HhSq4~TFu$d%B+x;-9tv5Y&P!%wF(5>lJ!O`Aqy)(6Clm98a z%X0I!EwZn*5=K)NAO4eyvxh#pg>grtT)oL>y@)1d4tu?n0+9T)cq~@fV;p@a*TEaG z_-4(O!~3`3E48SXYX{T2^UMBJ0I7u5NF{fo!lnMPjEr1qH8t!xt?I^TI%_JZtpQZ$ z^3@5H*V|?NWu7KbY7@x6(c=4;4_@$Vv!Unki8NsCi+L;`7A8h7&kop}=~m4S98(mZ zkQ%wPdscO}>Q_#YoJ~o#a<{}Z?(+(va;Ss3(efM-QlLA`DsB%$wLS9f7ATJJe!s$} z_A-{jQlEC)4*2#ncNfK2SlFx6hw1a}NMQRk_O?i2dqr%j49yIt7OL!DvcDIBw~tB% zN<1CDASD~G1K3H3h)Mom99*;i z(vr;mFD=P{|JIV!_&+!Nmoxv5m;SdTD&+qOjsHKrK=!%de>vF2LUVdGJH4?{gKV<~{3SwDG$iuhz!qh-9+B$Xpg)^WE^l zmxeaN7x@OojuN=n!fp($>#M^22Mds7wJyHT2QT)IGRT4Bfh1ZC?CvR5&%g#o?lTGS zr3U-ji{h#QI)*vlG-`x{3(_$uO>To(Rdv>>GU#=K>-1LEVVLu8;JInY4}T^rF&xK zJR;k|uY3#e!1K<%m?9P*K)HXlu@hAKObt(;kfpI#g{?1tt9!QAKmk6KE=_#bMV zJGpJ8iIbnmm&Mt>G&T_q4llJ*X7+N8AF|%`-Q7lgk5%B}9ULhLe#42ak7$4GI^k?f zAq}m*$77zT8Au1gMD*)n9p5~%!EY%#yPZj8n&VurbxQ&=H7DK)Omo-9uT;S$1OkPT zv)4GVEc4p(%yYpV?Z=5;K87`XT(d%7S~}Do$8vWnzbEl5ZpcX*T4S_gnZq@Y>;I$c z5Krqh)?c?@>Gsr7iE*qC;}=(VCw>cVxD~*H!u^I<4VSD>I*D98SofvG7G>w>K2hej zC#oAmVF@6;Oxxzekn5Zte+}`+`}^${P=C^AZ-sSiua$tH@;M~2+r+}obgJgmro3o4 zH<3=R&KPO>2j-srU)S*DG-@3F2?*!^y-yG77D4XGBlm?-?d!7SE|TpOI{ZUO0woi3 zQj(hZ&22!vZFmyBQAc8p17qw|kbsZr&XzseFn5IL-^C9}=(Wp%n%kN3Mx?Z6Yv8hc zezQA^+XqQKQ0lK!k~Yc6&E{N?;V7|)28YhJcX^zwj21fxqp&gKZ`#w18T-ej&-f(9 zWGaJo+k5toh}gNq(y1~n9t%q3y-+<1U&{&|KKOtw955t^2|5#04vJ>txP`c)#N?lFI&~Wx$Kf3lAw5kZ7qg*-qJ*&RFFv zj%OeRD=l&uv7odZsFl$eeWogATtWN^OZIHF{dTU?g{S(5DcySIYa`XaK)q*RwL=ML zz`bht2%KWpXb_u_>Jvj|aX%|Nuk|p*coc6CZwz+buo{B9eL{6{-?mS53~LC<>F`+) zVlSL}87{boX%u4Oid$VT5aC)~ zVy8+Qy_u@wHuko-M|_d)UKs0O5DPf~!Muc~DBqgcFI_=VaV%2l4Y^Jaml5r#rZfV* z0hVO7CN8UCftBch;W)KO3@fQ02bOLrE2mx?40@)jsIvHi(JwkhN<(qF;fisb`-J;y03p&NN z4?xLGwaQEHB|F+or~mzdQ=^Il^C56vAMx)|Sj9TUcNd8ALOWoToih%R-}7>pFG zdKtdSwS?w_>*TZQjhYZ$^7=?Ac#KGT)N2@vt)VF3wT2%T_DZj%-z_V?=VWoAhZrIG zzKpJ-GTv=Pl00-Q>o+xZWVghu{rMx;SloFABD{Pz@>yn-{yV7uFfi>U6puL;yFKnV zuUo2k0q8PG8yxlij4ZdfzMtGM`xyzIJ2N=Zkd^nd4^Ku%QjK=v9D%kiPh-?FzI#GG zTwY3!GnS&yJ+_JTDSOB;sH9pqV{it3mMFXPxrY^_@9Gk8kTaM zcKm~JV=Efc$vr<St@vVuX20cIfxuLUg>B_ao&UAfiGb)secRHWgye^8v)j+%c zu%R@3!o%Mlxw`yx+YF9>92L1V(bah{>FI%q+jRGF}c2hiiageG$_ZB3Nc|BYW)rg)E*7egQyWKRUI)i-}2` z4TtPW52Q#MkFa2b;rFUP1#`rW;uWBMpBKJ{;gKL@KXdhcWLc<~i5D>+6ZD>bHk=xA9dclNSlpuDCd? z{X*2Nw7iIf`MaVQW4!u_(BK+;yjXQ9GEE1t1(Xf`K89cDFtlS zve;nV8`c#ZJ|3bDqrKddE{h-EtQg~6^$5Tf(RyUw`>qu0Lh*?Iyte;Upaws9O6(eps{3@@ecKb5y@m0 z9omP426L6_6&S_q*)ezzJ>TQ-?+&sHK-l(2J7YuZnZmzuRTrG|q==etnr{q4oHr9h z;UtKsFUmBQH7tAC+Qr?BDws$=TqP2n{jP0@OFtzIYt)zRh|)JCHE3C@b$aZc#eQ>M z7}$6}7C@Tv>qyO~$)NbH!XJoE4M#SXwn_}|h=8|he!2*~ZkB&DYVv5y@#F#JDnmq5 z=S}ZSz?#tWfyB0bd~ZFVEVi36m88sy$U~eFRfb%rq0nk|GZExzd+^{&C)Z?d$M~RY znwYXAwA%cI*<2Ec`g~yyMPxfO2b^zB-Y#l`k3kyN7*A7wf(Ia9-+Wph2BBg6b83&5 zT=?8oQ%B66-KQ5Wjz@!RD`~ecXhkf<^h<$)^I3WvxHEUTa?9Q{^?g*fCmC|GrDhTR z9IRQ-JQI!r63}tv=N5&?vhvPkfV{bA^I4G+RV>{^KIs@gt@d8ZR$|MlYKkaA-tEHWp*G z*YE){p7Lk(^8t?|^DWh&Zd$6OX#O9<=-?*EW6>%g*aQ z5?l@W?NT^oF9eTQ`f>Q|)G~CR^s1{d~$TT|7Z1JpHhWfQY0#l_d0;p`Rpq| zQE37)Mzsd}9%Orx88k{6^=F1#UC5;w5?S9ceaUTxC$Qlez2K|tTR6sHh0+@I(Pa}3 z#1AE1Z47k3O?3~#8k~tl;+xC9g2bSm}yZez)_@o_5^{ z{jGvyHH9UzF|Wv9EAG?DouG(@ z&y132zeDvUAt^2dH}{3SjoS=$@k)bI?Q=F~?T6tvJc+xlL!!^CJ>uRL5}a(~5KUu6KCcFUA_c+E%`O z%hkcKdr!hD{-Rn>;uqJ}lkU^jMV6AHyPfU2*hQYq&xG`_)o*`$%c|UQ0~x-}h-+0e zJG9^Q$DZU5$q?EE)Zm;*^}V6Az^(a1j3t)G?W>vXQ|c*GCulP}ODuK%`L za5n+BgW>T-N`mdaHO3_aVhc{CP+S`-$=}n5q_EI_GRxPW_qp_?##+uJSKg{xuhg`G zl{DR5uw3c%=WN?7=Ty3|`1&s66RKwwvahR_vSa#s!|i^neyIFdtatIwpd^3&a6CU- z4knCP{pcWD|8kMEQup2WxA&fg5?zr{gMmWf1FgjGeEZEg_3uwd;N;(?MpAB$cq>Z~ z>0{JJ4-MND>yRaG>^~6^_HR^Q9_m79I-7wPyzVzajB>9xe?2W zx)`nas{46tD|kE3E1v7EO8SLUS+e;cxdGaeIqY;_%vGMoAl@E8j*NNM@L|Jusa>mr z^D2Co(niazr|+6B&&`%Nth4A4|JG)K@dN5s-6|1)pMgjfa;!bRIoNGZ>fUD5d?w_` zOdo7esQoaHA?W^8K~uPP&*riBfX_(+D~-BFlRtpxlR}gqd>Zoh94~u6siDwtMsUp_ z2>L3oB$F+$m*vV}aS|*3`RGJ_;i&_n*XLr~PM29Wvp={tO_|Eo$}@JCg&+(!z>&W> zv}K|(Pn}3q7g<|^Y>Mv}TpQ#Uc;QH3$Qb^`nJe9H4D3C6120><DYIJ6d zP2c!75^@Cb05ta`cvO=@_vnYw*)x1c)UNWseAsiP=P*s@OoMIVCi&X?!rPteOd1*JR=`Jo zfA-eOFbSg(gH_b0u+|MoM+=zDp5;1C=CU)zVLJKO3roS@PD|;|JB2E4QZ?ZZ7k zXWXzp$HGPyMK-nvH$*bChWuT~I+~A@p0o4)>uf|u0;`YvRPkwQC=5nRrv?kRr(lIp zyXyv)oRwUeRbDU)ts;ma5?P~gQJF^+c9(OojBeVjNlu`HZtP zyYlx(kUvS~W=3K3fjkf@E#w2`2sFfn-{9;{oPkA|4{0T|P>)z{{?_Gb$-b@QS_Gs+X`}yxvbu%mPbnl5^yN}PGkeXO_M41YW=(R zqwRWB#k#F-S6#HEkqjBb?9(=Y$UVvVQD=WmR-J-9)Stoqhy71-NGEMuhGc(_Cy?k8 zjcwU}h%1q+-UN7^1GXyuBl$vAas>TSSBwljqeveGaHki*h_spRi$52ZfDu_7hR z*p>J$iZ2#WA2!qS_2YUBraU76(8Tqo7N^%c>m{U`Z%6r44_I9OC83W>XH?CwRpLn)_CuXwp2zCv5vCDXPbzOy7)2e5XQtiH1RhwV# zH_VkOD2z7V5mA~s`K!uO#`**~-tEEfJyp5aa=H8MPmh+Xw?`{HhiqT#UWoI(fC+b9 zocb22R#9*g>Sc-B$lEzkkGmTBz3w>LS}Or#WHp%syF$hZ@6;pm_aX<;GS*dxbS@i* ztKy%&+J6Rk%!}x_x#eKq;ARz zgTHSPLQ%fmFO;i#;fWs8aEm=;fR_QUf0@g^spvAxWXEtb#-1+a4j6H$HXS_J6>Gv5 z{?-$GX9BY%_o@tP&2R`C!xMKS)~iS0F(BS05<%j=t9Z2uG*JXEMfYHZThpBGM(?leh z!rL}HrTi7f*b-tu(A5q9JzT6S!?(+X@+Qx(Bp`rjeN`EVi7Nfjsu{>JuV$0Ruy6IC z_A5K0^Smjld0I z@wOKMd zfnJ0VXEU_vJazUO_0fCIFCnvSJfr1!nadKkW_t>lqaItQYDn9&K(@x3daM0_^?yHK zV!DAk!f7ar$s-f1;}ntz@iztD2|Ccn2pXi{5f zJU&O^UZ0I~{)sPg1XS%CQ-}=q$+r~8yuGw}Z!PRy7Q1}bA4t=DO3*hy$RxM(-yrshh>;Myp%OmG%BpWUojbmnn&d@S%VJG8FzXeC1%O~?tr*q01Ay; z=H}0=*A&(3)$?L-c7>{Ld1tdUfh1J$!&P4mFL9MLH|$-ltNTNCaA)23$l2N95ic`mXCpXbjGWs=;J4&ks=1)D%(hlZrcCXMOa`)d;@qA7_zbwX<3LL2YyvY zc)C=XcaJ`k@U)_7s%pxt%M;GOsb0G<@7M7w_7s(h{jo+ivBxO1$M+A|x^h|<`;uci z)yHl$!OuG|6*+o#gZHN|D$wE^fp&*y!E@^;iKQiJ5$B!b=8)2B#x^!CMHbuSrKRzZ zsVcC6%yo-;Gdt<_1#X1A_?GF@Cnn1eK@bItUEbh?uW#Lw+YO z*q2nt&!OGpSzRc278Z9ehaGJl^}8QWJs+p5s9qQ5ikREzbSr!ts0%o1Q`O5k2Va0| z_18%W-`L;S(`k-tU1+a1J*=wdg~x&DYM-3c&jWN72LYbWZ~%8E%wsUJ6c)B;=pIGv7)<4+T-3Q1A+^U4bBRL z31?8O3*QAlB~o_-=|vu$pz${Ft8@2iuS#fD-8#jjg%5$NzN~~yNE~e`;iIz@-NPSS zUnK%xeH5%O34GJdZg$S61H86AaK1LjV%S-V_AWIK%2!mI;^Dl74Y6(MgMM$+!HqZE zy2iD>Eb{fXJv9Qu>CsFb=(^+~CbZb6H3igghN8Nv;~}DdJ=hmAPdlY(XnrS3s$=&m zHpQ^%t9(qlo)<~T>;@6JPA$0Q7ooJC8EC$nSqBTZ(dT_^DNyA!5m@{k;iv$XVNKY z%ZS|hkwx8BX@k-a;M`vYK~P${{tlEqtb5Zm5;BEe_bG45kk`b$*fD27cv@}|^tS-j;n z*4fFn`cf8!p3iQoL)FIQKdHXNSX#o(UuAE8(OVeCXWtezx2De54|sP%XpCN2a0Ti2 zpKTr7b(Q+=>xW^l6x5)1^d_?+#m~zFW=?-~aw_|4hff;^}kkfg9^?%7W{(Cq{!CCqjeu?fBqp@p>*yZc+GZxKTlPx&+Y0r$>7vi zwBYPQD($14O1g{O-+b$Q5^EUAZ7ywF_h&uDTpFQ~VBq$Ow+1RpmTt4QsV@ub&lH&D zZ-*mM+P_&Daa|)p+f4S^z=AdewqnLQp7qcYUPk_H?RbP;>TXUH>1E%;yL?_$)M737 zev!dglD|v6?X}$KIX515EodKMt&X3V{6@-Z%i?OwGAAkD6RP!j-uXN!ypiqYqpmM> zE(J8c#ErV5tifSU#P|9TeV+&_Gg9ZIC~%4W4;G*skeKP}*wJG&tJwT!gU2Ha!<6Qb zvqxKEdIRVD;s_)?i~Y(sQ(U|of5OG8Sg?2j`)x5!c=`_C;S4l#3}e$i!tS}$^LdTU z!K$s^^qr=3r=Og&6l?%-_WzvysPq%yhGA54f~KYk3-wuFCR{rQ82Y<#~iH@+ICu| zPO?1`y~lmz#nt^>`BJwb@6P1!z2y3YnW2k&*0m6?OhzeGJ37+RX%!NtzvQbs0rv8X6s@;X&g53n2C~{=xI7 z%?0raqdpcx%pl?_@z&YRrtg%w28?{FA`<;uU=Ne9CHaS>yWbH3$cQYS{`KEGU);T( zvpC2WMnF@>bVsG}%-I3ZV8ne!d;oiboyUVgaE=CpY|`{#&NsKNF*D^ae-O!#!8N|+ zCCvnd&iwMn@O!6;ikFee?37LaFiV_axSsK!`KyT4&-1{Qf|LZNVUfMckv7MrfmVq~ z`$ZQ1LRe3O@ixlTxhZd^#|Y!jJ1J6Ep1L^p$i#{652Qcd9aD#}Upc;lRB=})g-w&u zz44=Mlh@iiUu!Ger-KqVqs9Fr?WN9rai0%c+j|c`jk`y0YW#Om8|@upuX{qYcNaw6 z3i87G@^F&^SWf|{r6s@NjP+=mYywk_K)Q1-yuR^|F0LIujhNf5W*RL9M@kn(O2;D} zQJE=w30Y5iVHkojJpb_d1DkC>_dTgOx@$0h_qaqVQqDF5MmMby3T|b=k2JCKM*?Eu zXI-~TuC0ys-HkUd{My*`spJ9^$HS*|yJR$l`F7s9%to|IR(ubCWm~W3J*@>^ zOLgX^e(=;`Bwk_d1ymX8#Pgl6%;B`CsbLFy+WnvgZN)WriAZQ-E5_#|mV^4|xnt7( zz-jrv7zc0TZ=SlUa&kH#H*jXZC2Z{1ZL_#>sK@u607`^@b2Cuie`t_SiYOj^6bo?5 zJTf@Hm~o?6`G(|Vw)>xnMiX%_`ER}t9h=Qv|5^UM#Tb0d@DH@a4>EAdY3n?b$(dxq zyEbHh;%VoSnxK*{f3GZOu2zpHum)Q)Xfud`0n^YRh%J*qDTfm&*O|= zU<8I}!=xV}gHOn6JzE==`EpHSwf>1CE5xnX44&+1tu|I_7z6JZD7&{-8#1@tJcnVX zL5g6(m<2oV9g-o-SHF~?H<_D6>HT?`nXWyWfN73CojRzbD66BhJ9AB$irk&EMpxB* zy={Da+Fe;HifiiqWVTXkKDu6H*@Ttbi<_i{d%6A-@Ygm#GQP<^wSVl^yO{DcjwUXK9hX2_cnG5P%xbid9 zgfaB=zIZ7$3u=8AXo4>U)Ha{u2_hEUB3LO}2Lkdi;y{>K#O`Oq6F^^D$NlpYx;`!y z;THmuv2uQ{7-}7@lBNAgfuZJuYSs4A(h1{cw_!a+l!NiLx z5kWX;^{Xon7tNz(2IjnVQMaejQ1IqA0 z;_*lMd=F2zQ({)Iq#@XL$d>%LPryY??=`z1cY;E7&@k&%lCgJ&JbWa;AxDC>F#H*k zY9_FVWv6Cd%FZv$?;{E(^4VJNlWd_F&WII07fg`IC+SVHSd2qC{sGSmGx+iR=6?KD7nQwT44VL~k1MAO^6V zHg@%%Xd*%1@K|eFbNc(LSJ-jl*xl?uz*J(P`WwxOBexS_eYzRGTKT^kLvW&>oS%gL z@x2D6!u=wLF*i8bm1VBsasOJV{IV#BZ$+RwXCFv1ZTOk{y#6Z+#GXx4odc>=;ciVg znEpQ&eYGVRP0Yph{!+0C;m`M1H-ManL=bg3B$1UiOCe#~u4#P%ogm8nREut;*Mkx~>fxh|oN7l@f?P`JZrzZ0FKUyhSMsHuvI9*M@9SM{gir~qOq2ey3; z7U_SU{;RJ5TU^3QgfSM)^YF(Y!^O-8PNW?+z+`Y+#$I zCZr+)j%}HsM}gHYjz0oGi2k+j82t9YeB)CN>lQDExpqGaVT5*GWC>Kc(=`(GtfxTD zO9}8%`z$V0WDY_W^KMl+M6(r}crv@e)#tr!3DxTMS77~Dlb0KU&_-wE`z`>j65MIh z)BeYHqbSwGXGi1_dWe8G?aXHm{OVEb8Vy$Vu}$UShcl~FQy$lC7s729Zeaw;d2!vS z-JAtQE?`s`-&8TbQ=hWjj-pYc7TEAD6rl(#z@H#?XZS{5=tqmgl2b_M&u?JnW%g0= zf)~L^-Q~xg2GDGS48G@L3Hb{`MNODoHYYWqd@<65kD zj*$~m^I$`MLi@6*n}Zmw$$OFUgH@^2$7+*^0x?c`Z+~KY&FN;t#Xq?!4)W?}~@>ntc_$*6V|2OhI9s zbq=djn4v>py!(89SD5iOy^}dtJ!zI3WGc@`oGZL;nlUzGv44rJiyu{xG=3@l8@o&b zV`S=83~2A;pZ)G3hC_BIS2|%D5U8vL+(tX;wJqEqb&drg1r2nJBn>eoHLAChJwrC< zETNrgC0Eu$qV%GlSi>^9ROQ!@$5%R03g4|oS)b8Hh_RR@Qw+s^XvECdYlazq@AJcC zh#*m4W*}`1-({ikF{A$7za|?& zj$N2T0D;0dH&^0dtA!uBYB@GNb{H#4z0d?FK2ZG)vOtl{(KZ5SJT}g{-T*|&o?;`T z2)rb1%?mn(2d285ZeYdkjbtgxe245)bjtt9h#6k|4J@FSaInrYfgT1^y%3}KWGw`x zdYVwdoY>w3?I$F;gU>LqVm8DwLVvDi+hrOD0B@@eEb&^>p6o^&WH4qvVVb9x;L9%f zfmOX@R7KsjE>Vt?{)sytGVgD3&;k<~!Fy401kct|>y#V~`w91OsZ9JlY@nqzGbe8? zR{@aaBvI}7{oVhU+-O0o>x)*(@y338QC@nDWCew#-Csg^q^js&zgeNYAxZ_3OTWSv zQ9)}ZJ;SUKe14AWJUK%@DsuQIC(Q-zIlqDt0hE#hA7)>ax}!J)j>$!d>FZ;AJDT0t z^8^`@+DCy%RLercZ38*0*ZjsN*!TaLU;_A^wf&N^)-V=rR8DE@UYpTayNEhRer$6IHA(z4?f-~lMWeOuB?qCVWQAln8fULEI7<7~ zJ2tmYo7b^+f%pSv^ZX~;3CN!S>(8V};UGS{Dg>ON_aum?ui50~0gtX9!>=K{vs26G zi{;eNAAW|_E4&%Z^!}P7tfm2Spv9&VduqXES!x$lYu$rmHo)HFPkp#(6UX=0H|S`^ zPB5Ry0-&HkT9Q9+E+^J&UQt-x>E{UUqQ|j6CCUOfP)QLqftu|&h_uB+AS^>*T%E7^ ztc~0pkd>tTOg}WBAfA~be!fENBtJ`@w9}|wM#Vke_Myp;br;V`Z3Q(PRfh|otYj** z8;&H8jULa~^7cSk#@-s*zU4A8pujM#K6-3Kw5v~`S5vr_G%-fktRR0_^+6CMs~yG< zI-LND(qh7BxGEqL!oRufd$_a2y`sed1C4KV%1k8j2lEac-|ir{Y#Bk=+zX9@Z9_;) zfZ4(sV%mcV{kFUCI+A zM8qS$$$B5wJEn~)ht8rng_LZZ|AfTl7Lvs`XD_Hb8$4NyOf(i3i}A?cjZ}ZUkGwt} z`J$Iq(54y+K-de6Mi%XAlFa2~vVuriQsV4L`e?~$QXH9aI06=Px?`n;5|Km@Xqb;s z{~CcY*9C1dqdF&`rNL;ipkX=ImrZpY7x;z1n2$RL$>N62y zRS8@$9&+Ky$@l(>c_e!oK>;z0I;yB;PnxWW#F2@vk|(T^gNhbE6%)t5{ysD6|dD3}_MjcdC`obLg+p*$N~(_2kRHEBnJw;n)$v2enLPl7QG~xGgo9u-(lf{#)Y#R` z0F}$;Y64ap@ub`Q6J;NVltFAN%)p>%`cs&xsPqVkzI@#I^V^}I1HW#jU3P*Irr`rpwtwUqy{7iw@{Th1T>beo` zv{pM7^(Q>NZA;nO&oJ~rMFZlCS++D(cpRUYZ7&D#i6w3^E48CQw1jRamSEfM5rAxm zF*1}|kB(s}vRd6E?PX5cL-vv7Aq|&y&WL0M+jUz_ayerNNo>syu#EvXKn@)G$nL3& zl(nbX-!&G-CNq(XCri}Fi%N3%iQ1d_syTSW*MY94>l*XzRc&TewB&A7acgEEKuU$% zYNYW&F#A<|_i)Jb9rk^PEDfVGlg?)sYUu%^=CIRqJHzUpLx--Q4TzMc|4ktV57WRF zLzsH)YICd<+aTWQY&k5W9;5A+Ic2*i^6+fyjl~G)fMgpJl(LV4{}PsB{KVw3%3i2F zoyNbgmr({hl%@D=KYKc4qbA$+Y{1+#?oey-VtCxlpzxss56yx0o$o zNPbxVId-C%Z#2wZq0WMZ1I}wS<_mZw(I5ynIWeqIPd)%{MGPc!$Icer1Rkl>5|^C? z@!0O-q+Gtnjp$4dlmHQOZ#W)Pj4L~&p<}poI9k)vJ5(AjX^GZokEBdzZ(q*Uo(OAj zPhLOxqE){Uo>OP8K>g6?sK&X~a+oWN4DX<-~qc+~B?0nxL5{I(yyQ_@ zMoFZ)9AQyAbPVpWGdxxokbeaIFVfz!tBxjU7bZZk;1Jy1-8Sy-?(V_e-5oXrhv4q+ z?gV!YY~0=D<$m7toIh~B%vwD?wWhnLs;0Z-s(=3wUqvZ^M8V%e<;s()LE&0nUtrQ8 zTgu3(jH%e2Y{Ev!1B2Z0sLp}6H|K^CoiZL4bJMBaEi6XD`vUDEI%_(Gt>05ISJg-_ zu|VY~`7I%i7iZMAFfpz4N*uzSsk)`~M)Zr;9o}^}0@H%FM`#yWZ72a*jo|so8h`h+ zwXy~er?8x>uVpvJnh)?xi8u(Q4+Z7+2)2LgmA+5pON*LhV+&#tmQYoOinJ0(&%=2H zuWGouWHI4yMJub{#DW`0U>-U*Q%zCWDZBWQVm`UA|GN?ufxBR&w}Mke-61*ZHibf`tNBsdT(>)qK+Z zJk0b&XK#%>CdGFzIh65ELf=#g^LEUVX6Mi|9_^{nH4I@@Y52&0qyRCo8dBzLv=Bad!zYS~niEkFACt}U#2?m9SsH#p`q=@Ifs%G}6 zns$tpxUxjJan>IRIZDUM?lci^5yl@7aD2xq1xgUjtf71QN9M=vG_Mah5G-?<8(T~d z$cX57mpQs9QKEMOzK{c&?mg`^@+R-?X$LYag)g&~#S|p*c;P$8>{oM{nsD#hh(rc1 z0*(_S#ChK>tN^JD`uTD!K?#Fc=s4u4YRhKvrfb<`Rq27c;H_sX&haMn4ME!R<|kyy zJJQLw8bC64r1`ty;fGI`tH%ePiaO*AXTQ!i`2;PbYwUA>2T77*8|rBNx3z7l+u7r9NZjj#`ass zv8`}ae3-fak0?uiW*hZ>E4B29pP+id&YF@RIOxo? z`q_;=v{xjXT`7{$8OGxeDOb``_o+x;o=z*Ju!PM2NHDjxEoH~a4)E!_Y&(?ru_>HU z5EYdLfsU!Mt14=ecJ@IZ_MOo=7j;RXrHoKNF34|wBckYgl89gmQy3ucaMhQb-7ZLN zGFKq`O0krdamTO?AzW5YTA6MP2T*6#;E{L#Bj;sfS1iJ+sIoBRY}oC)xV%k%&dP@> z3Y^@(vGo!bk?;Tdm6a!Ff5>qq4Iub-((Nf#w)-MZNVGF@pOn_4&0yn6cOvHr z#bSJdA|9+W3NoJJlYM|ODQ>YpETAvWfwp2z=aLaoc?4{^CX+G>NRLpokO9STIat(S zr&LZ?Z#Fo3DsYy5+88gt5;~k2`c0oo?lSRrC-Pv9b+#Vg z^nh}A`vhwGhyqXWr)xc<{V(25zSq=FlE$pYbxuiq-gErS-u^L8J^A$PO_x?leBJFT z`@T#Qr|JPvdZuXReau=<bnyPL&Wd|?3!^?tq1M9KV5d|iA*#$s9(KI7o!le#u9F1PWalVdwr03g2z{W zy{Z}q@!b)E(N}rPc9wLA$rdqw()MdlGN=3TImU7qvKbYj6 zlz7&rg#n;^Ne|}Gq#%{t5ut)PK6fP-@%^-pXEHyWE4g%fD41{MzSr@bgh&0nmG37p zncdD6dvEwW1B~2T&RFK#tDx#;$Q`fil+j-lB1$JySxk$Woi|#h#tvNR#WO7_hkYSH zl^yLBsbBX*V&&)s^Gk)j4ew)ps0n&j6Gr=`#=OlDMOc!Om8HXv!92hxRpsVt#F-eu zIqAWRU02;dNl!p2+x$APyS_SlJSg2AeGB2yRH3KVug;-?Kf*g@wTM9P%BG! zf+LPEhU}C^)ofReeA_V6f8Dn@K;QMU&rYL2B{r-Pr0oYtQ5=lF9M7(aD#Jxh84-@05UR>QU8QCY~9)WjP zWP4Gf;(qQJ_X6vnTucyHWCm|&JLA&LYbxg~iYpm2%XKJ{7KaM-wTzU4e>T2^mz|(q z;^smeZLaS$-^muyD;MF=XM5LhQ5@fy_A4Z%P45(q3+^&XMV6$j6HHt7G+IxX4&ft= z$xrX6Gl5*978$CNxWVu(&{RrUsYR7Z-v;B#jTo2+o!atkZ^MKnj$ohK?Y4j4qmr~UQx?R$?~u4eSp{byPt1p9eQL(}irTpNpUNIVY0Z?;p`h&jJ@ zhta=Nfu26}oTd16N8^NlSe!$ockYLTb8|I){_yi^eYkO(>><(Ywqd9+-aDEvt|#&5 zxqM7(z~3_^Bq{-Iqil3|5!Wew=yOi~jVO$u!tc0AR9vOCisPCbv}0CaQrOU|e) z2a|U&CWuo6ThspMEiNcgW;?LFZ6rYKJQTVy>79S!&-@}NJTOaj9%0~~_t<`~k9cnG z%>DZ@=?TKxpcz;bmXxs57*Ojk^(>xg&4$n_fc{3i3pH=@4$AP!)YW+LfT_Dz@>Vp? zgo1`Fxg0emXn1VH!%e`zHAna~YSp$oE*zEYXoMoM?h^*Eg#k&5mX3n>Z+9UQmGE(1 zY^qz2_ul?bTbamc1ugHS%3*Nc-yMz8Q?np0CA#2A^BJzuh|p|bw?aWCpRc`SEm6po zM|^7XDP*X@Q*H7uM2FpZA)7GT+zrm9hnqI+c^>~^w#V-=()a}>X8nD9fwsY9z;t)B zJh0NW;qCSxCSVT*rakRBb(ef%`7FS@&@w#Fo|3OcZTwfSY3B6FrwiUZ|BSyIIB%Li zl@cn1D$$C_%^#s9x1`%bD>}cEy7E?%&F7|-U4;9z&cm75%JD;NPP$sbI}?Az$I~ty z@DP5;4Qm4SSJhriVe4iaSRHy*howSj;JKqSU!#;nIrCzzh)L&n}Oe`?<-FKPM=ZNNoA!ucrm_yE{6 z^aGiDtVSgCLYToShg9DjkunUnVwCfVo%&o=_YR)wO7LTzJ0U3mLhC_FKfIx+T@&bQ z(v&se#HHZN&`JgfI^ zlt%EqX1jyXI_Z43LisZ&JXe-7-jiS*|~{)=V<1!O9}NJ)F+Hf&{N*B<&?+m zQnEM!To_K0h-@UwIO#5%V1vx5>HU4<9$P1WD%)Rm7RB^p7dxO0p%LpJ@yU58zQjI2 zdisry@3ujgZD~Q-qfdAX!0J7{qICFbgo>t!Pf<0mu_bY+;0+ErnT`k zC&_JISyJ)!36dL6hf#3hX9h2(o!endz~~pn-W~+;dcJ@|i}8lshbx{>EDvYb1~r}! zwNjA7Y1jgB(VWHr^CIb#CdT<43V0X~IX8T3IyjYC=iP*BHlQ=Gb9j}q81oF+#74sy z```wW%d~%)!N?GO5O`t1&_06Er@xT+ zAH8I|4{oAtvv}{(BdrY6vLZ9y2*mYOeE0X80Vh`V5Z1MeZg3KfYHdSQQdoIL#Q(+; z(D~i-M)o_S&~l#;1uWM-k)qDE3aMPlCzy&+gu3sf>*(;ap`Gjnam-9m^&_u+?4y2l#tzzsiVp z^Ti%Z(;ust{L?hR(Dqkk;hQ*;53%PNTk7!~n}!Skio7Z)H+bM^yOJDJUwNkDN#;%F zq#Wr>uKajDElv`;ue^$ep`6W0iEUD6F?}B1Yba(BhB7R2^KPC7jJZB>H5W^TP?IRh zMTNex5K8C|cYAv)7)?T#-#LCM3bO^@ebC0x>H@}g3=3?~a7ljBzTv$Ixr!F~w=5z@ zRLrq`SI0kPOo;~H#ewe9vw?ao_8cou!!CLY@$Lktcs2sZb ztk(Id1^m*6LcI$qmQZ#)$oD@a1d|(aGrqxq^Jd7k<`Jb9vW*A}G!4HUu`6$JTf?d- zY}>i{G-ww1`r_ZX6+^`4nKk#c3gw(DZl*eWoS3FZCQrG?$869V#BmcTrpc7ra?k>$ z4-c4}eJ)cM5C!pQ>7A#^;-Wx4ZOhDHj+Icrn$r3TDG}J3H~SwI2j;b8Qs)n-!a>rw zVqqLnF$<~}P0LBXdoccQ?yiK!xLJH^LCY=DP3dRR%XPZYJIn=GXf`{lV6+C~;zJ1} z&<>8xGiT(Y$l7#6rpraOqNne8FLjP>$<|JQ@cN?QMB=g!4$_;M+19ArX=vyPO>! zo?sdKR~Czgwj{M;W2Nmcj{8td2R-3zwf_3-n_$j5wTJYtdd=;j4wD@z;WxR$rrYv* z|IQlbvQr%!lZ+K}EiU{eS*1H2EB@$;6FrT1&R70h5q5A^ym2cu0T*(cY;c}iwv^|0 z@VPvx^={fr?q&Z7VM2?4e0)?)upG=uV`!nNtdNC+hD9>?g_;4@PEKmBY8h-RS1|$S zEO4d=?sl0X!9|8<-N<86MZ@eno1vtWcZjQZ4;Oy+r5-CiBID=w@nsTXw5*6t{p`EO zE9_+GlTA1}t1G)8d`Nthi1Z`)Hv*cee_etxDHPtU$l&7%MqEU{09UCR0Eec`S$QSq z=Vn>+ioVv9Z#f$0W+R85dpQlS#PCv5wg!Xi(PQ+5^S68ATi2I3?ymJ%OfhpLBbgFQ zV2OhX+{5$phteCyLCbEEdF5d^n3m*uSi8wHfSp#}u!Y&2+ca6<11=m?Ra*}@m)V7d z&+PtUMuFXa6vE+JWAHEui-NPdr=&bQ(D8Dyvo5zt{HrHm@{A;oyB7@3m(0V!+U3hz zzPzs7i?qn18 z{LY9a(~cwsMK2__N3cm^`4M3+b6e}C)AU!{7?MmbcY3#NEQD%wdFL_M9kdWlkiy2s zfqiHrHiv(=)-SDx?Vyv1Qhs&f@9e;x7cU*i?`pEr6KvLp!0ekh{}@@%ej~_eRIfH*u`K%~28|eNq!R7Q;THYL>s2tf2KF zbn{V{5|z=HtzRfrq4hMWQb>7k4(XKu`SJ~p*vGU(tcLVGHMH>cmfb9k2iq=Q*CzXO z>$tm#!xb=m-1cAw!GhDYWTH#cpxIoNVCKgrQVw_fp4YV+?;v%lCjC9Tbb5dDu_U3L z;(HZK#;SX@;X^j9!-_Q(dy0z7_+Z|&^Q!ysdcJC~(5a_!idE(M1I_7MYoTrtyp4xt zM>xFT8okZiA-$h3z3TZ?hs!fBPUE@MQw7}sXOsz0<^mY zX`_-Sq4?sPmb8ug@6bqAU3c2)Se6}|8U3nqj21j@x#(9H zJfoo8^vklCuWZ==XWi}NShjofdAiFDH}hpL39-<4*n}q|rdl)}<5p!`(pt&3HR1$A zy(5)i_BMu#Rw;l%3cGSt@nl-;pT6~tR6uO8+zVb4hbDqfE=`7P)KMc{E2^Nd_K~+@ zu*T)v!RAv$-TVj`2R-d%@I2=cE~kub{1otZuWUBLPFVgEwlBky4*F|j-_YQ`tj~YJ zx?K#>uqrIy;+>?YIB4KJd^4Y-BBgSo)pW&u!?CB2(Tpy=K;T$=t*pX$SuX#mQ%2T5 zf#T^&p8SQbtX^#)U!Q@GpO@-nhbJ&jZ@u)hp~EynL|L21HclFP-5do5{3}W=48dIf zZP%?lzRKt!Nem0Z3hCeBu^~7|N4&2wH=p#Ln0aq zZn*mDVbzJqwP^y~@SK&Z@aWQaNo!Vp#ZSZ#2@E9k@1=&06boIPWds>Z zOA+bgi=0@lxWmn{qAGRJtTq>gdD5y#<`#!KvyW1e8770pq2B!Pi75+Q*$)Qj>$%x{ zJpB4I{0lS4((Y4ti`!+Lgyq;QJoY!n+S@T^DsYKE{#V~(ylHx0_JvjM;e!tW8TMLK zjvp)h8AVwqkonSb_*kQd0|Z}KN1~Lc!1Pp&y7{AdoK^GWTS^XDS9{5!y+JG9LUMW; zJ_e(Zzk^Ym;Q{D}xm#}b@I(tm9@1|hGc%&EUn0^avqsIWrOf*NW}5!5Wq2@8%It!o zR1Pkm?z#7}(~Km4T=W5MT#;C)M16np|*J*E7f{^R1J;)uuF;p-PR1j~`D z?2sCO>(u%9-WvnE8vkTD|A9 z=Pi2ER~2gdnbr(O+@^h8kIU}EFb#s*Jkk8aC5w;D6bPwN|06Z^?x+1f5ghCih*xWg z5Saep1gMCQK$2E(nG;)XP#(i30(bnKe78OeSx z3VpesR(>-BOZoI`>}e|V9ikMOI~|TrHdJ`h$}AZ2cAcsicc{zU$T_yoVD4g%jd#-z zWny*3i0^qm3P8QVtnSGyX$prifdCSc43PBKi?|E1(zL5RaK`&K_11oxr zg|8bjg|-l+HBHjDQUUpu^^O9*wLD?;2UvWC0}Lg%$m3plO7>4QI$WeJhL zINaxdsEoFI9_Z>YHO*VlWz?-GLyDt1id~=~aNRl^T6*8aPa7| z;CS$}HCh~18YMwXw;w%xhl{|^J({{cs5HNftY9Jx>9BMkuc*ZCEhIwL74xEfqHoPu zkyXXp8cY=6>w~Vqyz0XMdP4|(*|Xb*dkqwl`c@hrLaGUC*E0j5FM_`DquS{M{w}C_ z-1tf6M>6SFF^RA)V)8NqYc1WaT>7pCjY@V+iFx>6Vh)!(2Yne2y+UNk=MUpA5B}K~ z?FLCi!ptKBu-;FmV@|I2B*6E$tCoo%dunw`ulBHkBO``bh|^ZUMpEW_igB^K1l@(G zg1PycJRDPv)2f5|_S^JtA-^^|I`tEYmpz*+^bok-rQx>280H#w9VMB)%iswW@yPcu z0gjaG@j?XlWQ$VWFFl$(eir z4G~4$U8rl}pk9-b`aV`i=^)=8&Y+)1zU})`GrxFgoGY^1_4^s2gNio)a%f6LOd${EK*i2{OGL2G3q|55%K<} z&YO&Oq!8Z<&DRyEcCau@x{%4A7QKM}%*kqP!DczLCYG}@nOjovdZeKGxfN``dlj|P zy`A3^I%v)s5Z+>FR|eLouky&#xV^_#$;^40@{W==VYPdha*a*X_AzsQZ(AEZgQtE) z)PKgP7%(4!Dpkryyd|w&Qj@m}nAaL&cV;_nIUFO7u^dd4N3>tH8+++$QTGZ^P7^@|Kb)l;i(5y?< z4_S~@9Ly(xzTbjdmoiq2zoj!hnbf_p2-n^FpqZGk zJiEtO9pkeKEMqQ>{qj1S4^q`NT-L%u%Rk`PUrA;p9%GB|6@1G=-~aj8pybQU1!~&< z?_fdt83S+U6j=Y=1kd4Re0r7gXNgs%)+7u*6@{GOhsMO>H3vOILD}~`7R4C+aP;V9 zEfp5OMxIxAqG^a=BTj^nn_zT}sy@Sj(lV`(p4@GV z{eb|J^#L7)^oXODh)rS8_s6dT_ff9rOQZt#&F*t-1&WN+b~u89sc0#$KO+70ro{)6oAzo8c7Q)q^>Wb2#xpE1@N+PpDDCy3XfjO$Vd|%hB=>8jPZHgFS zjC>38tdKM3Wk0Sk4w6cdC+p4yD>sKFr(!5`tN=^=<+13eah+VNe94DN+}OpY?g|{9 zsfP81!5FshJD}kN$L7#gZ7^uS^FdzjkATR?oVI0R%FjtP%K$-#?=S%OR-(%R8R?2! ztjHPD^8pt3^s@Ui7a0&G@_u~YPW{{&Y^2g)$!-%?23bm31;$H{v%bNik`RkeRyW~B|w?u;j}lgx(*mh!b%TkzfLahICP3@TA-i=mRz zdP~#Y!$}5k-(9}qhDSKzp7X#9s_7h-c{F7@(i(>vu)|U0K}_fv=`y0h@5%W73n zhfv;qfrntv+Dt@IX^&gbx;j%!W@9YI6#O2Z*Z}i(S_W>70!dZ;4k<1m?wNGc-@dF< z(uh$dklz!JwKHw7d=`{;d^}a}dZN<3VYHGVndTrY*7WXNDA?a)7$Pho-@8_~Db7)wrV-svN z_cg=!FInJAU~;nQfwe;$b1CEPN00Bl>c0 z3&9MJxL^O9HZVM?TtGyrgs{bE$ni}~Lg(i*G!rH+bQAVLHC zO)A1YurEbKDQBdC)|0(u3n+!TO=e5Ms_zm19VGHLO>aEi=QB+2Qg&DgS~AlL3Af_4 zy_!Xo#e2PlqQMK`28z}6$Sh?o5cFp(B#aGuZ|&tEbZBwV_KFh-zEil_@=Bu-$$2Gs zZ+Qepj%`l(hyIb*?~fVbyg3ScOgrmJ)(68D{EG4$#r#U|yS&)_7!u#@SVwcBZy?m` zOTu~-kONS7iB>KiUSEi#aR}zH#T#_j#fXhnw8aaFO+iS z>44XL?)Ksk&kxX*mrYV!HH3x*BOxIbyFL(HFkJZt2a-ai&#Z zWJM*U5M$_jw|Rc{NpDEm>i~-kp1dPo!*3=iF`lr}6;#~)OUBoe6;XKcP9u`U7Fl}9 zCwrq4|0fnN#BJ{~uYAk@MuXzT=LnSSt^xJWZ^V|nZE`>V1Zo0{!{KdF-Z^@ z0UXMd?94Fs%r$JbuW2W6UY8PnIUtNA z^135J136LS5^<_KQQjA@7qZ7s0Fs~3x#8g|F3f*AKH=okcM}+`E-?blVJ@5ZxDM3ubQFT)94=LI)pwNoSy&{uzh6`~2nE5G z2wVGd(eB@Mbl=v)c>t+#4zO0K8f6=;d#HCy<~{QFslZ!DF=ag6v>IN-pUc0&_d`oQcmK3PiCA`B)Z7LaIpjE^KAwv*(D3P6ZfU=Up3Df# zyxM$ra*@tSvy7J0q{2Q-5nn0Nk)W?Ky*-<;j7kM6^pkUGLEy87nAP-Cb;c0c?GwlhM684m)*wX7-Uz4aj5Em<<_0rDH5Go@aDMjnDX|NkZZUPBwAc+5S zq%Z(3uXQtE=wN*Vk^$>a2Z2L97F=wm$A!6dFZp#B9Rkfg22Na7RkXCF| zE*Wt_s0^3$M?dG?Olc4B3$Pm`6%vSppA7UyN}YbhQDEO-@hQiTFQ|9NCn1bL>*(MR z{U^o9>+)`VkElC6y5UNg662443gg!C^v7UgnqO93x$xO-m%QZ$SnyC?Ic;rjE34y+ z@hCSQ`YIn)c4hdj8KCuFo;M+z=(rKSyo zDh|TSgQAWEJK-njm;61o)9Q-*Tt7Zk*P&-^l5c+dqjXxZ=!OeVnN#o2{wY3Tb7Y^} zuOrGpka;uLVI(%-dYx)VdMbZziAC4t%U z6ojg(<~!}VUU%SM0k60X6+1_i+j{HJrBr}yYn z>O0l+#FH4TB$Y@xYs}${x@?}_M5HAZE_9yt|0EbjLWzJUE?LC{Iw3zc(nvTX5f;SP zpH>hSbe`#XHkQ5jqHDPZxO-UVecL_6<38khZpmu#r_50~hhZN9dpS!dz*jBxRPGwHO*JFivoqDze>+pee=?8Cam7`7=2wfXqG_41l?wV&(z%liPLx8czg7K?Qbc#a^mc{zls z6dZjL{|&EXNxo9o^+*xg^FeuEbm5mGdZI)@&O%bBhxMyZOIz@sa=omFOY$$(_DEZ9 z*H`3y^abSZSBkh)Bo$x3rp}@joqgAM+d1E)*=5M8R>lr^(CBv6L2p9rGY%XauR=^k z*2aKdZqD0)T|7g;+}p0uq#ipA$d%~7zv%7}BD(~BzzM311(MNo^PDfb!0WH^9=hRK z@X_Q^kO_7odUXBQI65QF{}D`mf}~NgAHwk0y?cs}7y-KV;G+q{gDfOtU-@olE1yk2 z=bZ^naHn4Rf(P!xmPVjV_$$=`7U!Tp!eEh-A@Ua9Cy#2?XwMyOX z@~>HV8IH`WpIa?tinN@^k(p6t-2pF;+lA?Q&aLlWm^JZb^z^f>AAM{eoH2q@j1}fE zPQ_}SGiJ@6HzOt<36Kcr_C7Df#K*7VdFy4{RDW*Y@noyd9$OW#F(2jts2{d=^Jxx$ zoO-;+a7eh_XUKVD%gwTWtgBZb8^xy=QYli zA5$heBwCOx65U0hW5sVqF^>#KOiY!;B87*0XeSaIS#JVe+M&%@UaM+6Pk~y!NK}yi z8;AA3EP9%-Ib%d26ks4RAP%MpN>?3+&b&O@skDMHiHK6jf=n5H7;AXgQmGMe>$Rl9`N>g+o7D{apUWK*cEUNL9c; zUNp)`m&63E(Q^M^trj(NilQPnIs3#a^xSi+eN5^$QW8U;ir@tqJsoQ3X*NWZHW5-pry%9vrv7teYjcECZ~OJr z-*B!lkc~dCEKz?Zt`((5*&Mqm`$nsh!O*+}rz`if6 zNK*-<#_!7}ZWAg;P304bZs+#B_}yvH!nO_#k$bam|JL18HJhM}Nhoye%=DwxZ5w^P zG`l`@f|4t|H;6hu1{9eI)|p|iWwUha`mxM4wZRM@?9(?xdrt6di;WBv4B#n z9<)9-M1zYt&ft`n`Kf#C2`4Q4iiU|J`w3R+xI$g;Gf_6qp?`-B^te5A-0+IdRPp}| z8IpLXDLT^KuGnkX)*CE@F`V$<8fG(p9sSzEyX?PS1f$@ahah}_nT0jgEJ7BJkk@Er4T=3+ zhJG>FogGuIm?20MC^sD!{G&Q?f<%thL7cr<|6>155MkazKflQpD)fs6>P{pL<23#9 zJn5MY+AIHsVQv`Biou~`^{E=tWcP)fbsv^E2E&yx>B`Hn1R2^X(f5@l4v;>z-v;_t) zOMcLn+X2IIXGW>qyIm-MSqax#GS*E*4Gi!d4v%k>$PukW_zj<3wEWMJf%OfoS3BQ* zX;1`?4{b{*l@c|tK>g2L(w3R66OpE1IkL@SNQ<9s;Zb$2jw&@sw&$n%o%a9Dr=RhQ z$D!cRq!g-6WYHZgsTl=2P&5=;q#_tfhM(p&(h`jVqHJwGo|Pu2a_oRdYUu2|*SXtF zpRWm@7lIT&?_7ch|5hO-f_gxI2^YU$@(tgjeW}lnDX8xX->q@tH-Gh)T6+M^PPNUe z_?Ay7cKx}5X4pcCi4*m?{=I$xB$Fq9Oi%krgn=O<0&h_{zl!*y(ATj47I#gwpZRNq zT}j6|P6vLUG|4BL?^5m`?aC%uA2?R9*0mqmT{ZTFlbf*$1~MoLbHE?W$9*Y}96Bv~ zcY>sLWccUH{H9$sAj>s(vXbAocSvl!w`RH3)|Ccfd~ZxL7f4^p4vi+%RsF}la@#)c zw?2~`HZQV%iGK#I=nV%>!@0H-L2CQ|j-eTh{$a%VVaYC$`x7j@J|O(Z7LBoA3=%;A zs@G7|%Y)Kkug-xX6um5RWO^(~RhkcfI~O{$^>xuKfs2%*WDr9rBKLt!cDP#uR`V8!{+WsHT z&-{;3kBAW&DzkgboA@>nXK}?oN`t6dz*Ar9nPvRcBDeC9wAP!_Mp8|pDom%48u>Jc z(b1-m=>wwQ^m1Ehu^>kEI-%)rOnCH0vvA-p&R+aLXgk9#B z8chvaF9`0ZzOPUNb)vWS4Bkh%=b;^#_q!*syvp%~geo%i~M=L0% zHK5&M`xjrFTlRqCpv{6PSjm<|BlKfYORK5!7ktp2-7tR7Exn3aNs`-bAVtPRidLuv zuLpKD!c0W=nD8fyc>a*drZ!<=8iM5P>hsL_u`01%E1iL3+m9YrLguLBffWc>+Kh26 zw`Bn6W`x#hK$AX&!B3GdS4^qFqqK~KLHL-L-u7J4FwY)EF;r$d z?mz?rmDtlv40Q~|I7%;78Wa&I6BqJno%*;t4JLF7upH5 zn^Vt{(#`du^}$yy^?Ur(G~=U@my0ytO{R!ONPO4Z3x9>nckY07)FiALt0%;jsdRux z@$t8k*hmu>S;6^po~k`_OcPsYZ)-01LNMp=$awPF;;02C_eW}mwDiQEy~J(=)*pC9 z@p6Vf8Ii;g!87IH4&1bL+R|LUcVw^5GCmg?!-J5!_f6Z>*r*4-d^CJgAk-O7ubA7+ zhBE=J#ql5Q{e#zTE?^dHXEw|IJA94*quTQaUf^-qj;!7JcPh0Wztx(I#9jET=~r}a zAVitYZGV*7HH~lkqW3M3J4ae(5x2@vYrYbt=Qc7$i!HcNe2fT-Bi_Gz7M<^j^Xqk^ zJInZ2Qtx6n*zp_(2X_8^n{JSZ@V;QDC=}(8Q?b=Doyw393Z{LMl#TBm?)v;4v$X7j z|3DJg(-5H0LeNEhFs(E^NepTuCmSlw>{7U|`&ntwnsak-F|*^RhQ{IsP|3~Oa_K1t z_11e*oNn{4O`b#L6MDv$3CP@U)__eZBQnaF91J{&>@= zV&O{R!K@+kO^$y@anN#F>9GE0fOmUs?s8DK1gokf#rr_`$l__4FuiU|`B>-m?DoLv zNsS{e&=l|!c=wONXg%q3rzPAMPfi=H#}A|-iwI2@uKD0W@#?E;bgaUn$>L8ZC2nw8 zntj{RTz|KyJJm8LFwBh2%lCCZ%$(o7E~WPNo>Qx>z9SkPbqs25&bf(T^WVvCD#>LVw))ZeB8s(Jwa=L;`E$R!#zK z=Dn2geBw2ZbBIwsCi~YXc1Qbj-N^4Ak>m0Tzu8LVx5MQdAT6eQiZ4aet%~S&kv^I8 zNQ3>$7r^}ZA<301@{Keiy8Xdpxva_N!^uLnz_HJYY_8p^U`Z$rr7rnp(jS4ot_6Px4q+c%q9M$S&I6UFD zjAVcEqq3(f4MqNSb#_wyQ~|y(4?X|=<#~0!8z!)XXuP(Y&YPTX|M3O1P-i?P?#6kf zi2yY4Of+c8GqCmEGFp@Q{ujyx?5<}J+MkY8Z4uh}z;@$3E*@-_kgxXtXLeAbQ)LVG zmC1!(l5|Ku%xwH9MF(d-w#UG!T%FGi1kajVm8mp z0i~35_ZmUAV|#D4&BlBZ+Nm}f0E5RB)S7JCm?0T9?Vl?6_-^wtxN-eh$vI!hy2Cjk z#R9IX?B>CEFfXR62QKFikryn1oNY+LBpI&!?dd6`Q{{!-jd&hIVWbp1ODu~oVIcIj zXl3&cb6NxzM^FS7MBsad+!RdPr(E%0NeW~Kty^Zo(VfgWXIKIU<`DW3Q4cisFjvX!^6znO7U(-n!hV&YtJHO z5jl=Dc%YZ<_TF2cCvotG_e%W5FKYNL(S58Wy`73Fv7CP=v^iBw8%}Z=QT)OnH&O+g zqM4MCPx5YtvoJ(19)uJhv5d%I03DcoIcfp7Ry9& zy=bo^DP+T#cICLg>SC)bB>07MQUFRQMz#&%AsnNZn2E)p1zmUa|4M;fR~yPFxUP39w+{sHt6)648!{9nJ4p=_6pm^_2)qDUCBxT5w? zQOg;E#R(&snNyA&@bf^wnRHJ7ZCY@0zz=SUq2rGVmzCK{Fb$)O<($;b`M}Au`^iI# z4Rt?WC%~Sjbx{qV83JVL@Wyt|R#9~Wjj+(>nTL*;%fJG>rPy zMDz|)$H=n%iOHUh5mS7xu{9;6%QF~vNiuc~iOH1f_tx6uSP_Cx%Xtg1di3-sR832$AI-E(%%{Frm+-tM~H z)zwc|KV1cBW+<(sBg7z!qSp;9HP}2tVDp`1@%TVJxLEk=ikxXDEu5?P-h%a$6m}Nt z+dPfU&E909adM!E-(|fG+IM-wWLSG`K8NDu6C$A=kKEF2LzIbSBeNhRQL!hHMZyTN z=)=G0@((ZKfM;5Fi3Eks z-~qRXdTRolNF;(8>zHE^l*&|b&e604dG5$*)c&H+}9#YGoASP$w0+-AvowolG6QD*Oz)h;*=OCm

hZRjkB*03ox%o! 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

8x@#k^{5?=j^f_S)t=h7H1 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 literal 58615 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 diff --git a/docs/img/index/inline-errors01.png b/docs/img/index/inline-errors01.png index f5ef90ed714ea74a9acf0809fee0310fc011a597..b689c55624619fd9123c3fd377bb9745411115b6 100644 GIT binary patch literal 103453 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 )%!l}KZ_X3KN0kMGC}bWxbg{!uUX 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

?a0D>D8QNz$v>o8XzHq&#;D24C1*49RU1| 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

K+H z=JgQ@?x<=2c8mvK{UudY&nYDex;Q_uZP!FC9}@S6r&Osj<=orL`Pcn6f2PX`Gld41 z=Nqu!mjvk#pYzl5QLzz~+V+JRpW9rXpC=MSZgxYl3#pR9E-O5iTmwauQBb&a3hnQ} z+_tmqbmG)4r-DzhIP7piPT4oXWu>JiOn`;yZtS1b!12`-{-WByAK86RGZCh4Oc_nw z_9tgV;AITCt?DTOL3he72?TFy6=`U#8a%V)9WLP-KpuYfjf5h5$gfJ9Aoro)Z1)Rw z*=Bd_kUZuGTf*wCosVl(FyLxFY_F`b$l}Mp{_$dM$}#n?-upF`g5KX4;kGI1;Q|PtUqcLc1*=?3mPS9iXf)ygOGu zjOBR`XD|j(P($R?9Q|b&&A3JYvjt!|-1i!{@98TR? zuVx0WnA#l7^=&=~zqPS0Nz?AjO3Ozv`uwPYIH`F=Ma2cp&@xsfeu+(bFENZOGp{|l zDWdWpZ`S|-4(1^p5-b{#$irWiNU-`3q`n}vQzD&$By6Q1-SO}6KWO{!F#pZ}4F7Lg z{1?#wk7xD2Y618sApetC{nxDiS1tZ~VgFgdJ=aSbDyk21l{uJj(h>vIRdj#|CA;_k E2l{)B=l}o! diff --git a/docs/index.md b/docs/index.md index 0feec81c69..cd48b91385 100644 --- a/docs/index.md +++ b/docs/index.md @@ -118,16 +118,14 @@ And you want it to have this data: Then you could create a **SQLModel** model like this: ```Python -from typing import Optional - from sqlmodel import Field, SQLModel class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) name: str secret_name: str - age: Optional[int] = None + age: int | None = None ``` That class `Hero` is a **SQLModel** model, the equivalent of a SQL table in Python code. @@ -162,17 +160,15 @@ And **inline errors**: You can learn a lot more about **SQLModel** by quickly following the **tutorial**, but if you need a taste right now of how to put all that together and save to the database, you can do this: -```Python hl_lines="18 21 23-27" -from typing import Optional - +```Python hl_lines="16 19 21-25" from sqlmodel import Field, Session, SQLModel, create_engine class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) name: str secret_name: str - age: Optional[int] = None + age: int | None = None hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") @@ -198,17 +194,15 @@ That will save a **SQLite** database with the 3 heroes. Then you could write queries to select from that same database, for example with: -```Python hl_lines="15-18" -from typing import Optional - +```Python hl_lines="13-17" from sqlmodel import Field, Session, SQLModel, create_engine, select class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) name: str secret_name: str - age: Optional[int] = None + age: int | None = None engine = create_engine("sqlite:///database.db") diff --git a/docs/tutorial/automatic-id-none-refresh.md b/docs/tutorial/automatic-id-none-refresh.md index 7c7983c3d3..0e67633dee 100644 --- a/docs/tutorial/automatic-id-none-refresh.md +++ b/docs/tutorial/automatic-id-none-refresh.md @@ -4,7 +4,7 @@ In the previous chapter, we saw how to add rows to the database using **SQLModel Now let's talk a bit about why the `id` field **can't be `NULL`** on the database because it's a **primary key**, and we declare it using `Field(primary_key=True)`. -But the same `id` field actually **can be `None`** in the Python code, so we declare the type with `int | None (or Optional[int])`, and set the default value to `Field(default=None)`: +But the same `id` field actually **can be `None`** in the Python code, so we declare the type with `int | None`, and set the default value to `Field(default=None)`: {* ./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py ln[4:8] hl[5] *} @@ -18,15 +18,15 @@ When we create a new `Hero` instance, we don't set the `id`: {* ./docs_src/tutorial/automatic_id_none_refresh/tutorial001_py310.py ln[21:24] hl[21:24] *} -### How `Optional` Helps +### How `int | None` Helps Because we don't set the `id`, it takes the Python's default value of `None` that we set in `Field(default=None)`. -This is the only reason why we define it with `Optional` and with a default value of `None`. +This is the only reason why we define it with `int | None` and with a default value of `None`. Because at this point in the code, **before interacting with the database**, the Python value could actually be `None`. -If we assumed that the `id` was *always* an `int` and added the type annotation without `Optional`, we could end up writing broken code, like: +If we assumed that the `id` was *always* an `int` and added the type annotation without `int | None`, we could end up writing broken code, like: ```Python next_hero_id = hero_1.id + 1 @@ -38,7 +38,7 @@ If we ran this code before saving the hero to the database and the `hero_1.id` w TypeError: unsupported operand type(s) for +: 'NoneType' and 'int' ``` -But by declaring it with `Optional[int]`, the editor will help us to avoid writing broken code by showing us a warning telling us that the code could be invalid if `hero_1.id` is `None`. 🔍 +But by declaring it with `int | None`, the editor will help us to avoid writing broken code by showing us a warning telling us that the code could be invalid if `hero_1.id` is `None`. 🔍 ## Print the Default `id` Values diff --git a/docs/tutorial/connect/create-connected-rows.md b/docs/tutorial/connect/create-connected-rows.md index e91c351a16..b01d20eb2b 100644 --- a/docs/tutorial/connect/create-connected-rows.md +++ b/docs/tutorial/connect/create-connected-rows.md @@ -141,7 +141,7 @@ WHERE team.id = ? INFO Engine [cached since 0.001795s ago] (1,) ``` -There's something else to note. We marked `team_id` as `Optional[int]`, meaning that this could be `NULL` on the database (and `None` in Python). +There's something else to note. We marked `team_id` as `int | None`, meaning that this could be `NULL` on the database (and `None` in Python). That means that a hero doesn't have to have a team. And in this case, **Spider-Boy** doesn't have one. diff --git a/docs/tutorial/create-db-and-table.md b/docs/tutorial/create-db-and-table.md index 288e2bb20b..2f2f34c828 100644 --- a/docs/tutorial/create-db-and-table.md +++ b/docs/tutorial/create-db-and-table.md @@ -67,11 +67,9 @@ And the type of each of them will also be the type of table column: Let's now see with more detail these field/column declarations. -### Optional Fields, Nullable Columns +### `None` Fields, Nullable Columns -Let's start with `age`, notice that it has a type of `int | None (or Optional[int])`. - -And we import that `Optional` from the `typing` standard module. +Let's start with `age`, notice that it has a type of `int | None`. That is the standard way to declare that something "could be an `int` or `None`" in Python. @@ -81,21 +79,23 @@ And we also set the default value of `age` to `None`. /// tip -We also define `id` with `Optional`. But we will talk about `id` below. +We also define `id` with `int | None`. But we will talk about `id` below. /// -This way, we tell **SQLModel** that `age` is not required when validating data and that it has a default value of `None`. +Because the type is `int | None`: -And we also tell it that, in the SQL database, the default value of `age` is `NULL` (the SQL equivalent to Python's `None`). +* When validating data, `None` will be an allowed value for `age`. +* In the database, the column for `age` will be allowed to have `NULL` (the SQL equivalent to Python's `None`). -So, this column is "nullable" (can be set to `NULL`). +And because there's a default value `= None`: -/// info +* When validating data, this `age` field won't be required, it will be `None` by default. +* When saving to the database, the `age` column will have a `NULL` value by default. -In terms of **Pydantic**, `age` is an **optional field**. +/// tip -In terms of **SQLAlchemy**, `age` is a **nullable column**. +The default value could have been something else, like `= 42`. /// @@ -111,7 +111,7 @@ To do that, we use the special `Field` function from `sqlmodel` and set the argu That way, we tell **SQLModel** that this `id` field/column is the primary key of the table. -But inside the SQL database, it is **always required** and can't be `NULL`. Why should we declare it with `Optional`? +But inside the SQL database, it is **always required** and can't be `NULL`. Why should we declare it with `int | None`? The `id` will be required in the database, but it will be *generated by the database*, not by our code. @@ -128,7 +128,7 @@ somehow_save_in_db(my_hero) do_something(my_hero.id) # Now my_hero.id has a value generated in DB 🎉 ``` -So, because in *our code* (not in the database) the value of `id` *could be* `None`, we use `Optional`. This way **the editor will be able to help us**, for example, if we try to access the `id` of an object that we haven't saved in the database yet and would still be `None`. +So, because in *our code* (not in the database) the value of `id` *could be* `None`, we use `int | None`. This way **the editor will be able to help us**, for example, if we try to access the `id` of an object that we haven't saved in the database yet and would still be `None`. diff --git a/docs/tutorial/fastapi/multiple-models.md b/docs/tutorial/fastapi/multiple-models.md index 301c958d3f..1bc045612b 100644 --- a/docs/tutorial/fastapi/multiple-models.md +++ b/docs/tutorial/fastapi/multiple-models.md @@ -16,7 +16,7 @@ For input, we have: If we pay attention, it shows that the client *could* send an `id` in the JSON body of the request. -This means that the client could try to use the same ID that already exists in the database for another hero. +This means that the client could try to use the same ID that already exists in the database to create another hero. That's not what we want. @@ -51,7 +51,7 @@ The `age` is optional, we don't have to return it, or it could be `None` (or `nu Here's the weird thing, the `id` currently seems also "optional". 🤔 -This is because in our **SQLModel** class we declare the `id` with `Optional[int]`, because it could be `None` in memory until we save it in the database and we finally get the actual ID. +This is because in our **SQLModel** class we declare the `id` with a default value of `= None`, because it could be `None` in memory until we save it in the database and we finally get the actual ID. But in the responses, we always send a model from the database, so it **always has an ID**. So the `id` in the responses can be declared as required. @@ -71,7 +71,7 @@ And in most of the cases, the developer of the client for that API **will also b ### So Why is it Important to Have Required IDs -Now, what's the matter with having one **`id` field marked as "optional"** in a response when in reality it is always required? +Now, what's the matter with having one **`id` field marked as "optional"** in a response when in reality it is always available (required)? For example, **automatically generated clients** in other languages (or also in Python) would have some declaration that this field `id` is optional. @@ -98,7 +98,7 @@ But we also want to have a `HeroCreate` for the data we want to receive when **c * `secret_name`, required * `age`, optional -And we want to have a `HeroPublic` with the `id` field, but this time annotated with `id: int`, instead of `id: Optional[int]`, to make it clear that it is required in responses **read** from the clients: +And we want to have a `HeroPublic` with the `id` field, but this time with a type of `id: int`, instead of `id: int | None`, to make it clear that it will always have an `int` in responses **read** from the clients: * `id`, required * `name`, required @@ -225,7 +225,7 @@ Let's start with the only **table model**, the `Hero`: Notice that `Hero` now doesn't inherit from `SQLModel`, but from `HeroBase`. -And now we only declare one single field directly, the `id`, that here is `Optional[int]`, and is a `primary_key`. +And now we only declare one single field directly, the `id`, that here is `int | None`, and is a `primary_key`. And even though we don't declare the other fields **explicitly**, because they are inherited, they are also part of this `Hero` model. diff --git a/docs/tutorial/fastapi/update.md b/docs/tutorial/fastapi/update.md index 0aed115d6d..e3c8ac6ac7 100644 --- a/docs/tutorial/fastapi/update.md +++ b/docs/tutorial/fastapi/update.md @@ -8,15 +8,15 @@ We want clients to be able to update the `name`, the `secret_name`, and the `age But we don't want them to have to include all the data again just to **update a single field**. -So, we need to have all those fields **marked as optional**. +So, we need to make all those fields **optional**. -And because the `HeroBase` has some of them as *required* and not optional, we will need to **create a new model**. +And because the `HeroBase` has some of them *required* (without a default value), we will need to **create a new model**. /// tip Here is one of those cases where it probably makes sense to use an **independent model** instead of trying to come up with a complex tree of models inheriting from each other. -Because each field is **actually different** (we just change it to `Optional`, but that's already making it different), it makes sense to have them in their own model. +Because each field is **actually different** (we just set a default value of `None`, but that's already making it different), it makes sense to have them in their own model. /// diff --git a/docs/tutorial/many-to-many/create-models-with-link.md b/docs/tutorial/many-to-many/create-models-with-link.md index 36a0e10e7f..587fa436a8 100644 --- a/docs/tutorial/many-to-many/create-models-with-link.md +++ b/docs/tutorial/many-to-many/create-models-with-link.md @@ -46,7 +46,7 @@ We **removed** the previous `team_id` field (column) because now the relationshi The relationship attribute is now named **`teams`** instead of `team`, as now we support multiple teams. -It is no longer an `Optional[Team]` but a list of teams, annotated as **`list[Team]`**. +It no longer has a type of `Team | None` but a list of teams, the type is now declared as **`list[Team]`**. We are using the **`Relationship()`** here too. diff --git a/docs/tutorial/relationship-attributes/define-relationships-attributes.md b/docs/tutorial/relationship-attributes/define-relationships-attributes.md index 2646082c38..c5307d3663 100644 --- a/docs/tutorial/relationship-attributes/define-relationships-attributes.md +++ b/docs/tutorial/relationship-attributes/define-relationships-attributes.md @@ -68,15 +68,15 @@ if hero.team: print(hero.team.name) ``` -## Optional Relationship Attributes +## Relationship Attributes or `None` -Notice that in the `Hero` class, the type annotation for `team` is `Optional[Team]`. +Notice that in the `Hero` class, the type annotation for `team` is `Team | None`. This means that this attribute could be `None`, or it could be a full `Team` object. This is because the related **`team_id` could also be `None`** (or `NULL` in the database). -If it was required for a `Hero` instance to belong to a `Team`, then the `team_id` would be `int` instead of `Optional[int]`, its `Field` would be `Field(foreign_key="team.id")` instead of `Field(default=None, foreign_key="team.id")` and the `team` attribute would be a `Team` instead of `Optional[Team]`. +If it was required for a `Hero` instance to belong to a `Team`, then the `team_id` would be `int` instead of `int | None`, its `Field` would be `Field(foreign_key="team.id")` instead of `Field(default=None, foreign_key="team.id")` and the `team` attribute would be a `Team` instead of `Team | None`. ## Relationship Attributes With Lists diff --git a/docs/tutorial/where.md b/docs/tutorial/where.md index 7e0fe97c4c..b6d08e72fa 100644 --- a/docs/tutorial/where.md +++ b/docs/tutorial/where.md @@ -690,7 +690,7 @@ It would be an error telling you that > `Hero.age` is potentially `None`, and you cannot compare `None` with `>` -This is because as we are using pure and plain Python annotations for the fields, `age` is indeed annotated as `int | None (or Optional[int])`. +This is because as we are using pure and plain Python annotations for the fields, `age` is indeed annotated as `int | None`. By using this simple and standard Python type annotations we get the benefit of the extra simplicity and the inline error checks when creating or using instances. ✨ From 43f24c1ae118c9d771abf06612b6c01cc86de854 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 27 Apr 2025 18:54:00 +0000 Subject: [PATCH 619/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 644e6b7262..5fff3322b2 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Docs +* 📝 Update all docs references to `Optional` to use the new syntax in Python 3.10, e.g. `int | None`. PR [#1351](https://github.com/fastapi/sqlmodel/pull/1351) by [@tiangolo](https://github.com/tiangolo). * 📝 Update install and usage with FastAPI CLI in FastAPI tutorial. PR [#1350](https://github.com/fastapi/sqlmodel/pull/1350) by [@tiangolo](https://github.com/tiangolo). * 📝 Update FastAPI tutorial docs to use the new `model.sqlmodel_update()` instead of old `setattr()`. PR [#1117](https://github.com/fastapi/sqlmodel/pull/1117) by [@jpizquierdo](https://github.com/jpizquierdo). * ✏️ Update `docs/virtual-environments.md`. PR [#1321](https://github.com/fastapi/sqlmodel/pull/1321) by [@sylvainHellin](https://github.com/sylvainHellin). From 7956254b962e87c690f1a8ba54a2a76c4ad895bc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 27 Apr 2025 18:59:37 +0000 Subject: [PATCH 620/906] =?UTF-8?q?=E2=AC=86=20Bump=20typer=20from=200.12.?= =?UTF-8?q?3=20to=200.15.2=20(#1325)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [typer](https://github.com/fastapi/typer) from 0.12.3 to 0.15.2. - [Release notes](https://github.com/fastapi/typer/releases) - [Changelog](https://github.com/fastapi/typer/blob/master/docs/release-notes.md) - [Commits](https://github.com/fastapi/typer/compare/0.12.3...0.15.2) --- updated-dependencies: - dependency-name: typer dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 318103451b..a7c796946b 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -13,6 +13,6 @@ cairosvg==2.7.1 # mkdocstrings[python]==0.25.1 griffe-typingdoc==0.2.5 # For griffe, it formats with black -typer == 0.12.3 +typer == 0.15.2 mkdocs-macros-plugin==1.0.5 markdown-include-variants==0.0.4 From 21a3978eac88816159ed47ed6cd2c8c1760a6a2c Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 27 Apr 2025 18:59:53 +0000 Subject: [PATCH 621/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 5fff3322b2..354e08205b 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -15,6 +15,7 @@ ### Internal +* ⬆ Bump typer from 0.12.3 to 0.15.2. PR [#1325](https://github.com/fastapi/sqlmodel/pull/1325) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump httpx from 0.24.1 to 0.28.1. PR [#1238](https://github.com/fastapi/sqlmodel/pull/1238) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump astral-sh/setup-uv from 5 to 6. PR [#1348](https://github.com/fastapi/sqlmodel/pull/1348) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Update pytest requirement from <8.0.0,>=7.0.1 to >=7.0.1,<9.0.0. PR [#1022](https://github.com/fastapi/sqlmodel/pull/1022) by [@dependabot[bot]](https://github.com/apps/dependabot). From cd98b247f33c4732dceb49c514e10192b13356b5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 11:05:00 +0200 Subject: [PATCH 622/906] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#1339)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.11.2 → v0.11.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.2...v0.11.6) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sofie Van Landeghem --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6aa7c458d6..51ca24ba5d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.2 + rev: v0.11.6 hooks: - id: ruff args: From 3a8121f2b712b7fa7759413679cc69e6ef3b4ad7 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 28 Apr 2025 09:05:41 +0000 Subject: [PATCH 623/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 354e08205b..494660a5cc 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -15,6 +15,7 @@ ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1339](https://github.com/fastapi/sqlmodel/pull/1339) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump typer from 0.12.3 to 0.15.2. PR [#1325](https://github.com/fastapi/sqlmodel/pull/1325) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump httpx from 0.24.1 to 0.28.1. PR [#1238](https://github.com/fastapi/sqlmodel/pull/1238) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump astral-sh/setup-uv from 5 to 6. PR [#1348](https://github.com/fastapi/sqlmodel/pull/1348) by [@dependabot[bot]](https://github.com/apps/dependabot). From a7e8323cf425b90c5690d8b94f901a9161447dd1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 17:00:13 +0200 Subject: [PATCH 624/906] =?UTF-8?q?=E2=AC=86=20Bump=20typer=20from=200.15.?= =?UTF-8?q?2=20to=200.15.3=20(#1357)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [typer](https://github.com/fastapi/typer) from 0.15.2 to 0.15.3. - [Release notes](https://github.com/fastapi/typer/releases) - [Changelog](https://github.com/fastapi/typer/blob/master/docs/release-notes.md) - [Commits](https://github.com/fastapi/typer/compare/0.15.2...0.15.3) --- updated-dependencies: - dependency-name: typer dependency-version: 0.15.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index a7c796946b..58722b9892 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -13,6 +13,6 @@ cairosvg==2.7.1 # mkdocstrings[python]==0.25.1 griffe-typingdoc==0.2.5 # For griffe, it formats with black -typer == 0.15.2 +typer == 0.15.3 mkdocs-macros-plugin==1.0.5 markdown-include-variants==0.0.4 From dd7b41927fc59c6a8ea0b8753c60652e2e97438e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 17:00:30 +0200 Subject: [PATCH 625/906] =?UTF-8?q?=E2=AC=86=20Bump=20typing-extensions=20?= =?UTF-8?q?from=204.12.2=20to=204.13.2=20(#1356)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [typing-extensions](https://github.com/python/typing_extensions) from 4.12.2 to 4.13.2. - [Release notes](https://github.com/python/typing_extensions/releases) - [Changelog](https://github.com/python/typing_extensions/blob/main/CHANGELOG.md) - [Commits](https://github.com/python/typing_extensions/compare/4.12.2...4.13.2) --- updated-dependencies: - dependency-name: typing-extensions dependency-version: 4.13.2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 935f47cdab..0bdffb2e4f 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -9,4 +9,4 @@ fastapi >=0.103.2 httpx ==0.28.1 dirty-equals ==0.9.0 jinja2 ==3.1.6 -typing-extensions ==4.12.2 +typing-extensions ==4.13.2 From 957ea1b2e08b9db5d7e3b492596e033fe45aaaf4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 30 Apr 2025 15:00:32 +0000 Subject: [PATCH 626/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 494660a5cc..153da4ae0d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -15,6 +15,7 @@ ### Internal +* ⬆ Bump typer from 0.15.2 to 0.15.3. PR [#1357](https://github.com/fastapi/sqlmodel/pull/1357) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1339](https://github.com/fastapi/sqlmodel/pull/1339) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump typer from 0.12.3 to 0.15.2. PR [#1325](https://github.com/fastapi/sqlmodel/pull/1325) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump httpx from 0.24.1 to 0.28.1. PR [#1238](https://github.com/fastapi/sqlmodel/pull/1238) by [@dependabot[bot]](https://github.com/apps/dependabot). From b760273d81fdaecd25c25de7acb16ce48fb5f8c3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 17:00:50 +0200 Subject: [PATCH 627/906] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#1353)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.11.6 → v0.11.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.6...v0.11.7) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 51ca24ba5d..b1429a5624 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.6 + rev: v0.11.7 hooks: - id: ruff args: From 2b0f5c93a186ffb751d4974564907d407d61e9dd Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 30 Apr 2025 15:00:54 +0000 Subject: [PATCH 628/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 153da4ae0d..a6f333178f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -15,6 +15,7 @@ ### Internal +* ⬆ Bump typing-extensions from 4.12.2 to 4.13.2. PR [#1356](https://github.com/fastapi/sqlmodel/pull/1356) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump typer from 0.15.2 to 0.15.3. PR [#1357](https://github.com/fastapi/sqlmodel/pull/1357) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1339](https://github.com/fastapi/sqlmodel/pull/1339) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump typer from 0.12.3 to 0.15.2. PR [#1325](https://github.com/fastapi/sqlmodel/pull/1325) by [@dependabot[bot]](https://github.com/apps/dependabot). From 048bc2dff1a8c4d4ef3679b2d4dce6f62da2b112 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 30 Apr 2025 15:01:19 +0000 Subject: [PATCH 629/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index a6f333178f..6fb51f349b 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -15,6 +15,7 @@ ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1353](https://github.com/fastapi/sqlmodel/pull/1353) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump typing-extensions from 4.12.2 to 4.13.2. PR [#1356](https://github.com/fastapi/sqlmodel/pull/1356) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump typer from 0.15.2 to 0.15.3. PR [#1357](https://github.com/fastapi/sqlmodel/pull/1357) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1339](https://github.com/fastapi/sqlmodel/pull/1339) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From 957185ca22bc739ff9db7b941967bbd8b638c674 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 17:01:33 +0200 Subject: [PATCH 630/906] =?UTF-8?q?=E2=AC=86=20Bump=20ruff=20from=200.9.6?= =?UTF-8?q?=20to=200.11.7=20(#1355)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ruff](https://github.com/astral-sh/ruff) from 0.9.6 to 0.11.7. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.9.6...0.11.7) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.11.7 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 0bdffb2e4f..864051d30e 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -3,7 +3,7 @@ pytest >=7.0.1,<9.0.0 coverage[toml] >=6.2,<8.0 mypy ==1.4.1 -ruff ==0.9.6 +ruff ==0.11.7 # For FastAPI tests fastapi >=0.103.2 httpx ==0.28.1 From 6e8707528d86e718628f01039e916d8983ff53a1 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 30 Apr 2025 15:02:27 +0000 Subject: [PATCH 631/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 6fb51f349b..c0e0cdf537 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -15,6 +15,7 @@ ### Internal +* ⬆ Bump ruff from 0.9.6 to 0.11.7. PR [#1355](https://github.com/fastapi/sqlmodel/pull/1355) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1353](https://github.com/fastapi/sqlmodel/pull/1353) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump typing-extensions from 4.12.2 to 4.13.2. PR [#1356](https://github.com/fastapi/sqlmodel/pull/1356) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump typer from 0.15.2 to 0.15.3. PR [#1357](https://github.com/fastapi/sqlmodel/pull/1357) by [@dependabot[bot]](https://github.com/apps/dependabot). From 76f048e7b4d9dffedc435e8b5fa9e3242c42bedc Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Fri, 2 May 2025 14:51:40 -0700 Subject: [PATCH 632/906] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Tweak=20the=20gram?= =?UTF-8?q?mar=20in=20`docs/learn/index.md`=20(#1363)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/learn/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/learn/index.md b/docs/learn/index.md index 5a8253c4f6..bcf8a0b0f5 100644 --- a/docs/learn/index.md +++ b/docs/learn/index.md @@ -4,4 +4,4 @@ Learn how to use **SQLModel** here. This includes an introduction to **databases**, **SQL**, how to interact with databases from **code** and more. -You could consider this a **book**, a **course**, the **official** and recommended way to learn **SQLModel**. 😎 +You could consider this a **book**, a **course**, and the **official** recommended way to learn **SQLModel**. 😎 From 74feaa56b90165f0a0810cc98510838f91cf36f9 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 2 May 2025 21:52:01 +0000 Subject: [PATCH 633/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index c0e0cdf537..4d28c00d4b 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Docs +* ✏️ Tweak the grammar in `docs/learn/index.md`. PR [#1363](https://github.com/fastapi/sqlmodel/pull/1363) by [@brettcannon](https://github.com/brettcannon). * 📝 Update all docs references to `Optional` to use the new syntax in Python 3.10, e.g. `int | None`. PR [#1351](https://github.com/fastapi/sqlmodel/pull/1351) by [@tiangolo](https://github.com/tiangolo). * 📝 Update install and usage with FastAPI CLI in FastAPI tutorial. PR [#1350](https://github.com/fastapi/sqlmodel/pull/1350) by [@tiangolo](https://github.com/tiangolo). * 📝 Update FastAPI tutorial docs to use the new `model.sqlmodel_update()` instead of old `setattr()`. PR [#1117](https://github.com/fastapi/sqlmodel/pull/1117) by [@jpizquierdo](https://github.com/jpizquierdo). From e2cad050bed2592aa3295e04ca1b78168c91ae96 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 6 May 2025 10:06:24 +0200 Subject: [PATCH 634/906] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#1367)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.11.7 → v0.11.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.7...v0.11.8) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b1429a5624..2a688e2b42 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.7 + rev: v0.11.8 hooks: - id: ruff args: From 2230d9d8a542c63bb872ee5ffe4f3bebe29c4f3a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 May 2025 10:06:44 +0200 Subject: [PATCH 635/906] =?UTF-8?q?=E2=AC=86=20Bump=20pillow=20from=2011.0?= =?UTF-8?q?.0=20to=2011.2.1=20(#1361)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [pillow](https://github.com/python-pillow/Pillow) from 11.0.0 to 11.2.1. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/11.0.0...11.2.1) --- updated-dependencies: - dependency-name: pillow dependency-version: 11.2.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 58722b9892..e717e71c98 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -7,7 +7,7 @@ pyyaml >=5.3.1,<7.0.0 # For Material for MkDocs, Chinese search # jieba==0.42.1 # For image processing by Material for MkDocs -pillow==11.0.0 +pillow==11.2.1 # For image processing by Material for MkDocs cairosvg==2.7.1 # mkdocstrings[python]==0.25.1 From 78d9d5179d51361e6cbe6caefebee9c5f4a5f69b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 May 2025 10:06:56 +0200 Subject: [PATCH 636/906] =?UTF-8?q?=E2=AC=86=20Update=20pre-commit=20requi?= =?UTF-8?q?rement=20from=20<4.0.0,>=3D2.17.0=20to=20>=3D2.17.0,<5.0.0=20(#?= =?UTF-8?q?1360)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates the requirements on [pre-commit](https://github.com/pre-commit/pre-commit) to permit the latest version. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v2.17.0...v4.2.0) --- updated-dependencies: - dependency-name: pre-commit dependency-version: 4.2.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1e21c5d2f3..f17705f380 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,4 @@ -r requirements-tests.txt -r requirements-docs.txt -pre-commit >=2.17.0,<4.0.0 +pre-commit >=2.17.0,<5.0.0 From 86cc480610c6ba0fc4474c3a780382d94940369c Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 6 May 2025 08:06:58 +0000 Subject: [PATCH 637/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 4d28c00d4b..23af6176c9 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -16,6 +16,7 @@ ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1367](https://github.com/fastapi/sqlmodel/pull/1367) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump ruff from 0.9.6 to 0.11.7. PR [#1355](https://github.com/fastapi/sqlmodel/pull/1355) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1353](https://github.com/fastapi/sqlmodel/pull/1353) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump typing-extensions from 4.12.2 to 4.13.2. PR [#1356](https://github.com/fastapi/sqlmodel/pull/1356) by [@dependabot[bot]](https://github.com/apps/dependabot). From 3fdfdae2550832174dd289752769487961570709 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 May 2025 10:07:09 +0200 Subject: [PATCH 638/906] =?UTF-8?q?=E2=AC=86=20Bump=20griffe-typingdoc=20f?= =?UTF-8?q?rom=200.2.5=20to=200.2.8=20(#1359)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [griffe-typingdoc](https://github.com/mkdocstrings/griffe-typingdoc) from 0.2.5 to 0.2.8. - [Release notes](https://github.com/mkdocstrings/griffe-typingdoc/releases) - [Changelog](https://github.com/mkdocstrings/griffe-typingdoc/blob/main/CHANGELOG.md) - [Commits](https://github.com/mkdocstrings/griffe-typingdoc/compare/0.2.5...0.2.8) --- updated-dependencies: - dependency-name: griffe-typingdoc dependency-version: 0.2.8 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index e717e71c98..70b7a43dbe 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -11,7 +11,7 @@ pillow==11.2.1 # For image processing by Material for MkDocs cairosvg==2.7.1 # mkdocstrings[python]==0.25.1 -griffe-typingdoc==0.2.5 +griffe-typingdoc==0.2.8 # For griffe, it formats with black typer == 0.15.3 mkdocs-macros-plugin==1.0.5 From 1170c92c5cf91e7b29c119d81d126c954e06933b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 May 2025 10:07:21 +0200 Subject: [PATCH 639/906] =?UTF-8?q?=E2=AC=86=20Bump=20mkdocs-macros-plugin?= =?UTF-8?q?=20from=201.0.5=20to=201.3.7=20(#1354)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mkdocs-macros-plugin](https://github.com/fralau/mkdocs_macros_plugin) from 1.0.5 to 1.3.7. - [Release notes](https://github.com/fralau/mkdocs_macros_plugin/releases) - [Changelog](https://github.com/fralau/mkdocs-macros-plugin/blob/master/CHANGELOG.md) - [Commits](https://github.com/fralau/mkdocs_macros_plugin/compare/v1.0.5...v1.3.7) --- updated-dependencies: - dependency-name: mkdocs-macros-plugin dependency-version: 1.3.7 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 70b7a43dbe..ca92568732 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -14,5 +14,5 @@ cairosvg==2.7.1 griffe-typingdoc==0.2.8 # For griffe, it formats with black typer == 0.15.3 -mkdocs-macros-plugin==1.0.5 +mkdocs-macros-plugin==1.3.7 markdown-include-variants==0.0.4 From 47c476b8bc15b08b07eac347200b4e2ea6a72846 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 6 May 2025 08:08:16 +0000 Subject: [PATCH 640/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 23af6176c9..adbabc9e45 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -16,6 +16,7 @@ ### Internal +* ⬆ Bump pillow from 11.0.0 to 11.2.1. PR [#1361](https://github.com/fastapi/sqlmodel/pull/1361) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1367](https://github.com/fastapi/sqlmodel/pull/1367) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump ruff from 0.9.6 to 0.11.7. PR [#1355](https://github.com/fastapi/sqlmodel/pull/1355) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1353](https://github.com/fastapi/sqlmodel/pull/1353) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From 72b4557ce91011f7c1f36399bfbad86027c9536c Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 6 May 2025 08:08:23 +0000 Subject: [PATCH 641/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index adbabc9e45..5f7269f428 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -16,6 +16,7 @@ ### Internal +* ⬆ Update pre-commit requirement from <4.0.0,>=2.17.0 to >=2.17.0,<5.0.0. PR [#1360](https://github.com/fastapi/sqlmodel/pull/1360) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pillow from 11.0.0 to 11.2.1. PR [#1361](https://github.com/fastapi/sqlmodel/pull/1361) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1367](https://github.com/fastapi/sqlmodel/pull/1367) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump ruff from 0.9.6 to 0.11.7. PR [#1355](https://github.com/fastapi/sqlmodel/pull/1355) by [@dependabot[bot]](https://github.com/apps/dependabot). From d290787ada13d72ff4303e6e58073e5e2772339e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A0=82=E7=B3=96=E6=A9=98?= <54745033+Foxerine@users.noreply.github.com> Date: Tue, 6 May 2025 16:08:46 +0800 Subject: [PATCH 642/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20`docs/tutorial/?= =?UTF-8?q?fastapi/relationships.md`=20(#1365)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tutorial/fastapi/relationships.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/fastapi/relationships.md b/docs/tutorial/fastapi/relationships.md index 1b00043901..f789fd930a 100644 --- a/docs/tutorial/fastapi/relationships.md +++ b/docs/tutorial/fastapi/relationships.md @@ -50,7 +50,7 @@ Now, remember that - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/img/databases/external-server.drawio.svg b/docs/img/databases/external-server.drawio.svg new file mode 100644 index 0000000000..f86b5dc1e6 --- /dev/null +++ b/docs/img/databases/external-server.drawio.svg @@ -0,0 +1,778 @@ + + + + + + + + + + + + + + + +
+
+
+ + Machine / Computer + +
+
+
+
+ + Machine / Computer + +
+
+
+ + + + + + + + + + + + + + + + + + + +
+
+
+ + Database application + +
+
+
+
+ + Database application + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + File + +
+
+
+
+ + File + +
+
+
+ + + + + + + +
+
+
+ + Data + +
+
+
+
+ + Data + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + File + +
+
+
+
+ + File + +
+
+
+ + + + + + + +
+
+
+ + Data + +
+
+
+
+ + Data + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + File + +
+
+
+
+ + File + +
+
+
+ + + + + + + +
+
+
+ + Data + +
+
+
+
+ + Data + +
+
+
+ + + + + + + + + + + +
+
+
+ + Machine / Computer + +
+
+
+
+ + Machine / Computer + +
+
+
+ + + + + + + +
+
+
+ + Your code + +
+
+
+
+ + Your code + +
+
+
+ + + + +
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/databases/external-server.svg b/docs/img/databases/external-server.svg deleted file mode 100644 index 4a85c58225..0000000000 --- a/docs/img/databases/external-server.svg +++ /dev/null @@ -1 +0,0 @@ -
Machine / Computer
Machine / Computer
Database application
Database application
File
File
Data
Data
File
File
Data
Data
File
File
Data
Data
Machine / Computer
Machine / Computer
Your code
Your code
Viewer does not support full SVG 1.1
diff --git a/docs/img/databases/multiple-servers.drawio b/docs/img/databases/multiple-servers.drawio deleted file mode 100644 index 9a4fd542f2..0000000000 --- a/docs/img/databases/multiple-servers.drawio +++ /dev/null @@ -1,205 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/img/databases/multiple-servers.drawio.svg b/docs/img/databases/multiple-servers.drawio.svg new file mode 100644 index 0000000000..b7370592a0 --- /dev/null +++ b/docs/img/databases/multiple-servers.drawio.svg @@ -0,0 +1,903 @@ + + + + + + + + + + + + + + + +
+
+
+ + Machine / Computer + +
+
+
+
+ + Machine / Computer + +
+
+
+ + + + + + + + + + + + + + + +
+
+
+ + Database application + +
+
+
+
+ + Database application + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + File + +
+
+
+
+ + File + +
+
+
+ + + + + + + +
+
+
+ + Data + +
+
+
+
+ + Data + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + File + +
+
+
+
+ + File + +
+
+
+ + + + + + + +
+
+
+ + Data + +
+
+
+
+ + Data + +
+
+
+ + + + + + + + + + + +
+
+
+ + Machine / Computer + +
+
+
+
+ + Machine / Computer + +
+
+
+ + + + + + + +
+
+
+ + Your code + +
+
+
+
+ + Your code + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + Machine / Computer + +
+
+
+
+ + Machine / Computer + +
+
+
+ + + + + + + + + + + + + + + +
+
+
+ + Database application + +
+
+
+
+ + Database application + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + File + +
+
+
+
+ + File + +
+
+
+ + + + + + + +
+
+
+ + Data + +
+
+
+
+ + Data + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + File + +
+
+
+
+ + File + +
+
+
+ + + + + + + +
+
+
+ + Data + +
+
+
+
+ + Data + +
+
+
+ + + + + + + + + + +
+
+
+ + Machine / Computer + +
+
+
+
+ + Machine / Computer + +
+
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + Database application + +
+
+
+
+ + Database application + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + File + +
+
+
+
+ + File + +
+
+
+ + + + + + + +
+
+
+ + Data + +
+
+
+
+ + Data + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + File + +
+
+
+
+ + File + +
+
+
+ + + + + + + +
+
+
+ + Data + +
+
+
+
+ + Data + +
+
+
+ + + + + + + + + + + + + + +
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/databases/multiple-servers.svg b/docs/img/databases/multiple-servers.svg deleted file mode 100644 index 083d0e73bd..0000000000 --- a/docs/img/databases/multiple-servers.svg +++ /dev/null @@ -1 +0,0 @@ -
Machine / Computer
Machine / Computer
Database application
Database application
File
File
Data
Data
File
File
Data
Data
Machine / Computer
Machine / Computer
Your code
Your code
Machine / Computer
Machine / Computer
Database application
Database application
File
File
Data
Data
File
File
Data
Data
Machine / Computer
Machine / Computer
Database application
Database application
File
File
Data
Data
File
File
Data
Data
Viewer does not support full SVG 1.1
diff --git a/docs/img/databases/relationships.drawio b/docs/img/databases/relationships.drawio deleted file mode 100644 index 85d45e95ad..0000000000 --- a/docs/img/databases/relationships.drawio +++ /dev/null @@ -1,151 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/img/databases/relationships.drawio.svg b/docs/img/databases/relationships.drawio.svg new file mode 100644 index 0000000000..e1cbb43e92 --- /dev/null +++ b/docs/img/databases/relationships.drawio.svg @@ -0,0 +1,1189 @@ + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + hero + +
+
+
+
+ + hero + +
+
+
+ + + + + + + + + + + +
+
+
+ + + id + + +
+
+
+
+ + id + +
+
+
+ + + + + + + + +
+
+
+ + + name + + +
+
+
+
+ + name + +
+
+
+ + + + + + + + +
+
+
+ + + secret_name + + +
+
+
+
+ + secret_name + +
+
+
+ + + + + + + + +
+
+
+ + + age + + +
+
+
+
+ + age + +
+
+
+ + + + + + + + +
+
+
+ + + team_id + + +
+
+
+
+ + team_id + +
+
+
+ + + + + + + + + + + +
+
+
+ + + 1 + + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + +
+
+
+ + + Deadpond + + +
+
+
+
+ + Deadpond + +
+
+
+ + + + + + + + +
+
+
+ + + Dive Wilson + + +
+
+
+
+ + Dive Wilson + +
+
+
+ + + + + + + + +
+
+
+ + + null + + +
+
+
+
+ + null + +
+
+
+ + + + + + + + +
+
+
+ + + 2 + + +
+
+
+
+ + 2 + +
+
+
+ + + + + + + + + + + +
+
+
+ + + 2 + + +
+
+
+
+ + 2 + +
+
+
+ + + + + + + + +
+
+
+ + + Spider-Boy + + +
+
+
+
+ + Spider-Boy + +
+
+
+ + + + + + + + +
+
+
+ + + Pedro Parqueador + + +
+
+
+
+ + Pedro Parqueador + +
+
+
+ + + + + + + + +
+
+
+ + + null + + +
+
+
+
+ + null + +
+
+
+ + + + + + + + +
+
+
+ + + 1 + + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + + + + +
+
+
+ + + 3 + + +
+
+
+
+ + 3 + +
+
+
+ + + + + + + + +
+
+
+ + + Rusty-Man + + +
+
+
+
+ + Rusty-Man + +
+
+
+ + + + + + + + +
+
+
+ + + Tommy Sharp + + +
+
+
+
+ + Tommy Sharp + +
+
+
+ + + + + + + + +
+
+
+ + + 48 + + +
+
+
+
+ + 48 + +
+
+
+ + + + + + + + +
+
+
+ + + 1 + + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + + + + + + +
+
+
+ + team + +
+
+
+
+ + team + +
+
+
+ + + + + + + + + + + +
+
+
+ + + id + + +
+
+
+
+ + id + +
+
+
+ + + + + + + + +
+
+
+ + + name + + +
+
+
+
+ + name + +
+
+
+ + + + + + + + +
+
+
+ + + headquarters + + +
+
+
+
+ + headquarters + +
+
+
+ + + + + + + + + + + +
+
+
+ + + 1 + + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + +
+
+
+ + + Preventers + + +
+
+
+
+ + Preventers + +
+
+
+ + + + + + + + +
+
+
+ + + Sharp Tower + + +
+
+
+
+ + Sharp Tower + +
+
+
+ + + + + + + + + + + +
+
+
+ + + 2 + + +
+
+
+
+ + 2 + +
+
+
+ + + + + + + + +
+
+
+ + + Z-Force + + +
+
+
+
+ + Z-Force + +
+
+
+ + + + + + + + +
+
+
+

+ + Sister Margaret's Bar + +

+
+
+
+
+ + Sister Margaret's Bar + +
+
+
+ + + + + + + + + + + + +
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/databases/relationships.svg b/docs/img/databases/relationships.svg deleted file mode 100644 index bea9aa803c..0000000000 --- a/docs/img/databases/relationships.svg +++ /dev/null @@ -1,57 +0,0 @@ -
hero
hero
id
id
name
name
secret_name
secret_name
age
age
team_id
team_id
1
1
Deadpond
Deadpond
Dive Wilson
Dive Wilson
null
null
2
2
2
2
Spider-Boy
Spider-Boy
Pedro Parqueador
Pedro Parqueador
null
null
1
1
3
3
Rusty-Man
Rusty-Man
Tommy Sharp
Tommy Sharp
48
48
1
1
team
team
id
id
name
name
headquarters
headquarters
1
1
Preventers
Preventers
Sharp Tower
Sharp Tower
2
2
Z-Force
Z-Force

Sister Margaret's Bar

Sister Margaret's Bar
Viewer does not support full SVG 1.1
diff --git a/docs/img/databases/same-server.drawio b/docs/img/databases/same-server.drawio deleted file mode 100644 index 4f43be43cb..0000000000 --- a/docs/img/databases/same-server.drawio +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/img/databases/same-server.drawio.svg b/docs/img/databases/same-server.drawio.svg new file mode 100644 index 0000000000..d9aabff8e9 --- /dev/null +++ b/docs/img/databases/same-server.drawio.svg @@ -0,0 +1,425 @@ + + + + + + + + + + + + + + + +
+
+
+ + Machine / Computer + +
+
+
+
+ + Machine / Computer + +
+
+
+ + + + + + + + + + + + + + + + + + + +
+
+
+ + Database application + +
+
+
+
+ + Database application + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + File + +
+
+
+
+ + File + +
+
+
+ + + + + + + +
+
+
+ + Data + +
+
+
+
+ + Data + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + File + +
+
+
+
+ + File + +
+
+
+ + + + + + + +
+
+
+ + Data + +
+
+
+
+ + Data + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + File + +
+
+
+
+ + File + +
+
+
+ + + + + + + +
+
+
+ + Data + +
+
+
+
+ + Data + +
+
+
+ + + + + + + + + + + +
+
+
+ + Your code + +
+
+
+
+ + Your code + +
+
+
+
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/databases/same-server.svg b/docs/img/databases/same-server.svg deleted file mode 100644 index a3f6dff36b..0000000000 --- a/docs/img/databases/same-server.svg +++ /dev/null @@ -1 +0,0 @@ -
Machine / Computer
Machine / Computer
Database application
Database application
File
File
Data
Data
File
File
Data
Data
File
File
Data
Data
Your code
Your code
Viewer does not support full SVG 1.1
diff --git a/docs/img/databases/single-file.drawio b/docs/img/databases/single-file.drawio deleted file mode 100644 index c379f71c1c..0000000000 --- a/docs/img/databases/single-file.drawio +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/img/databases/single-file.drawio.svg b/docs/img/databases/single-file.drawio.svg new file mode 100644 index 0000000000..e92ae0ca3b --- /dev/null +++ b/docs/img/databases/single-file.drawio.svg @@ -0,0 +1,119 @@ + + + + + + + + + + + + + +
+
+
+ + Machine / Computer + +
+
+
+
+ + Machine / Computer + +
+
+
+ + + + + + + + + + + +
+
+
+ + Your code + +
+
+
+
+ + Your code + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + File: heroes.db + +
+
+
+
+ + File: heroes.db + +
+
+
+ + + + + + + +
+
+
+ + Data + +
+
+
+
+ + Data + +
+
+
+
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/databases/single-file.svg b/docs/img/databases/single-file.svg deleted file mode 100644 index 52c91e573d..0000000000 --- a/docs/img/databases/single-file.svg +++ /dev/null @@ -1 +0,0 @@ -
Machine / Computer
Machine / Computer
Your code
Your code
File: heroes.db
File: heroes.db
Data
Data
Viewer does not support full SVG 1.1
diff --git a/docs/img/db-to-code/mapper.drawio b/docs/img/db-to-code/mapper.drawio deleted file mode 100644 index 3b002eb3f8..0000000000 --- a/docs/img/db-to-code/mapper.drawio +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/img/db-to-code/mapper.drawio.svg b/docs/img/db-to-code/mapper.drawio.svg new file mode 100644 index 0000000000..e402e4280a --- /dev/null +++ b/docs/img/db-to-code/mapper.drawio.svg @@ -0,0 +1,291 @@ + + + + + + + + + + + + + + + + + + + + +
+
+
+ + Set of triangles + +
+
+
+
+ + Set of triangles + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + Set of squares + +
+
+
+
+ + Set of squares + +
+
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + Squares to Triangles + + Mapper + + +
+
+
+
+ + Squares to Triangles Mapp... + +
+
+
+ + + +
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/db-to-code/mapper.svg b/docs/img/db-to-code/mapper.svg deleted file mode 100644 index e31a464bba..0000000000 --- a/docs/img/db-to-code/mapper.svg +++ /dev/null @@ -1 +0,0 @@ -
Set of triangles
Set of triangles
Set of squares
Set of squares
Squares to Triangles Mapper
Squares to Triangles Mapp...
Viewer does not support full SVG 1.1
diff --git a/docs/img/tutorial/indexes/dictionary001.drawio b/docs/img/tutorial/indexes/dictionary001.drawio deleted file mode 100644 index 84992d0213..0000000000 --- a/docs/img/tutorial/indexes/dictionary001.drawio +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/img/tutorial/indexes/dictionary001.drawio.svg b/docs/img/tutorial/indexes/dictionary001.drawio.svg new file mode 100644 index 0000000000..75ebe51f43 --- /dev/null +++ b/docs/img/tutorial/indexes/dictionary001.drawio.svg @@ -0,0 +1,660 @@ + + + + + + + + + + + + + +
+
+
+ + A + +
+
+
+
+ + A + +
+
+
+ + + + + + + +
+
+
+ + B + +
+
+
+
+ + B + +
+
+
+ + + + + + + +
+
+
+ + C + +
+
+
+
+ + C + +
+
+
+ + + + + + + +
+
+
+ + D + +
+
+
+
+ + D + +
+
+
+ + + + + + + +
+
+
+ + E + +
+
+
+
+ + E + +
+
+
+ + + + + + + +
+
+
+ + F + +
+
+
+
+ + F + +
+
+
+ + + + + + + +
+
+
+ + G + +
+
+
+
+ + G + +
+
+
+ + + + + + + +
+
+
+ + H + +
+
+
+
+ + H + +
+
+
+ + + + + + + +
+
+
+ + I + +
+
+
+
+ + I + +
+
+
+ + + + + + + +
+
+
+ + J + +
+
+
+
+ + J + +
+
+
+ + + + + + + +
+
+
+ + K + +
+
+
+
+ + K + +
+
+
+ + + + + + + +
+
+
+ + L + +
+
+
+
+ + L + +
+
+
+ + + + + + + +
+
+
+ + M + +
+
+
+
+ + M + +
+
+
+ + + + + + + +
+
+
+ + N + +
+
+
+
+ + N + +
+
+
+ + + + + + + +
+
+
+ + O + +
+
+
+
+ + O + +
+
+
+ + + + + + + +
+
+
+ + P + +
+
+
+
+ + P + +
+
+
+ + + + + + + +
+
+
+ + Q + +
+
+
+
+ + Q + +
+
+
+ + + + + + + +
+
+
+ + R + +
+
+
+
+ + R + +
+
+
+ + + + + + + +
+
+
+ + S + +
+
+
+
+ + S + +
+
+
+ + + + + + + +
+
+
+ + T + +
+
+
+
+ + T + +
+
+
+ + + + + + + +
+
+
+ + U + +
+
+
+
+ + U + +
+
+
+ + + + + + + +
+
+
+ + V + +
+
+
+
+ + V + +
+
+
+ + + + + + + +
+
+
+ + W + +
+
+
+
+ + W + +
+
+
+ + + + + + + +
+
+
+ + X + +
+
+
+
+ + X + +
+
+
+ + + + + + + +
+
+
+ + Y + +
+
+
+
+ + Y + +
+
+
+ + + + + + + +
+
+
+ + Z + +
+
+
+
+ + Z + +
+
+
+ + + + + + + +
+
+
+ + Dictionary + +
+
+
+
+ + Dictionary + +
+
+
+ + + + + + + +
+
+
+ + M + +
+
+
+
+ + M + +
+
+
+
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/tutorial/indexes/dictionary001.svg b/docs/img/tutorial/indexes/dictionary001.svg deleted file mode 100644 index 59fc39294e..0000000000 --- a/docs/img/tutorial/indexes/dictionary001.svg +++ /dev/null @@ -1,57 +0,0 @@ -
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
M
M
Viewer does not support full SVG 1.1
diff --git a/docs/img/tutorial/indexes/dictionary002.drawio b/docs/img/tutorial/indexes/dictionary002.drawio deleted file mode 100644 index 52544abde9..0000000000 --- a/docs/img/tutorial/indexes/dictionary002.drawio +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/img/tutorial/indexes/dictionary002.drawio.svg b/docs/img/tutorial/indexes/dictionary002.drawio.svg new file mode 100644 index 0000000000..b468c90607 --- /dev/null +++ b/docs/img/tutorial/indexes/dictionary002.drawio.svg @@ -0,0 +1,660 @@ + + + + + + + + + + + + + +
+
+
+ + A + +
+
+
+
+ + A + +
+
+
+ + + + + + + +
+
+
+ + B + +
+
+
+
+ + B + +
+
+
+ + + + + + + +
+
+
+ + C + +
+
+
+
+ + C + +
+
+
+ + + + + + + +
+
+
+ + D + +
+
+
+
+ + D + +
+
+
+ + + + + + + +
+
+
+ + E + +
+
+
+
+ + E + +
+
+
+ + + + + + + +
+
+
+ + F + +
+
+
+
+ + F + +
+
+
+ + + + + + + +
+
+
+ + G + +
+
+
+
+ + G + +
+
+
+ + + + + + + +
+
+
+ + H + +
+
+
+
+ + H + +
+
+
+ + + + + + + +
+
+
+ + I + +
+
+
+
+ + I + +
+
+
+ + + + + + + +
+
+
+ + J + +
+
+
+
+ + J + +
+
+
+ + + + + + + +
+
+
+ + K + +
+
+
+
+ + K + +
+
+
+ + + + + + + +
+
+
+ + L + +
+
+
+
+ + L + +
+
+
+ + + + + + + +
+
+
+ + M + +
+
+
+
+ + M + +
+
+
+ + + + + + + +
+
+
+ + N + +
+
+
+
+ + N + +
+
+
+ + + + + + + +
+
+
+ + O + +
+
+
+
+ + O + +
+
+
+ + + + + + + +
+
+
+ + P + +
+
+
+
+ + P + +
+
+
+ + + + + + + +
+
+
+ + Q + +
+
+
+
+ + Q + +
+
+
+ + + + + + + +
+
+
+ + R + +
+
+
+
+ + R + +
+
+
+ + + + + + + +
+
+
+ + S + +
+
+
+
+ + S + +
+
+
+ + + + + + + +
+
+
+ + T + +
+
+
+
+ + T + +
+
+
+ + + + + + + +
+
+
+ + U + +
+
+
+
+ + U + +
+
+
+ + + + + + + +
+
+
+ + V + +
+
+
+
+ + V + +
+
+
+ + + + + + + +
+
+
+ + W + +
+
+
+
+ + W + +
+
+
+ + + + + + + +
+
+
+ + X + +
+
+
+
+ + X + +
+
+
+ + + + + + + +
+
+
+ + Y + +
+
+
+
+ + Y + +
+
+
+ + + + + + + +
+
+
+ + Z + +
+
+
+
+ + Z + +
+
+
+ + + + + + + +
+
+
+ + Dictionary + +
+
+
+
+ + Dictionary + +
+
+
+ + + + + + + +
+
+
+ + M + +
+
+
+
+ + M + +
+
+
+
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/tutorial/indexes/dictionary002.svg b/docs/img/tutorial/indexes/dictionary002.svg deleted file mode 100644 index d612925125..0000000000 --- a/docs/img/tutorial/indexes/dictionary002.svg +++ /dev/null @@ -1 +0,0 @@ -
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
M
M
Viewer does not support full SVG 1.1
diff --git a/docs/img/tutorial/indexes/dictionary003.drawio b/docs/img/tutorial/indexes/dictionary003.drawio deleted file mode 100644 index d37353364c..0000000000 --- a/docs/img/tutorial/indexes/dictionary003.drawio +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/img/tutorial/indexes/dictionary003.drawio.svg b/docs/img/tutorial/indexes/dictionary003.drawio.svg new file mode 100644 index 0000000000..2a036508f6 --- /dev/null +++ b/docs/img/tutorial/indexes/dictionary003.drawio.svg @@ -0,0 +1,640 @@ + + + + + + + + + + + + + +
+
+
+ + A + +
+
+
+
+ + A + +
+
+
+ + + + + + + +
+
+
+ + B + +
+
+
+
+ + B + +
+
+
+ + + + + + + +
+
+
+ + C + +
+
+
+
+ + C + +
+
+
+ + + + + + + +
+
+
+ + D + +
+
+
+
+ + D + +
+
+
+ + + + + + + +
+
+
+ + E + +
+
+
+
+ + E + +
+
+
+ + + + + + + +
+
+
+ + F + +
+
+
+
+ + F + +
+
+
+ + + + + + + +
+
+
+ + G + +
+
+
+
+ + G + +
+
+
+ + + + + + + +
+
+
+ + H + +
+
+
+
+ + H + +
+
+
+ + + + + + + +
+
+
+ + I + +
+
+
+
+ + I + +
+
+
+ + + + + + + +
+
+
+ + J + +
+
+
+
+ + J + +
+
+
+ + + + + + + +
+
+
+ + K + +
+
+
+
+ + K + +
+
+
+ + + + + + + +
+
+
+ + L + +
+
+
+
+ + L + +
+
+
+ + + + + + + +
+
+
+ + M + +
+
+
+
+ + M + +
+
+
+ + + + + + + +
+
+
+ + N + +
+
+
+
+ + N + +
+
+
+ + + + + + + +
+
+
+ + O + +
+
+
+
+ + O + +
+
+
+ + + + + + + +
+
+
+ + P + +
+
+
+
+ + P + +
+
+
+ + + + + + + +
+
+
+ + Q + +
+
+
+
+ + Q + +
+
+
+ + + + + + + +
+
+
+ + R + +
+
+
+
+ + R + +
+
+
+ + + + + + + +
+
+
+ + S + +
+
+
+
+ + S + +
+
+
+ + + + + + + +
+
+
+ + T + +
+
+
+
+ + T + +
+
+
+ + + + + + + +
+
+
+ + U + +
+
+
+
+ + U + +
+
+
+ + + + + + + +
+
+
+ + V + +
+
+
+
+ + V + +
+
+
+ + + + + + + +
+
+
+ + W + +
+
+
+
+ + W + +
+
+
+ + + + + + + +
+
+
+ + X + +
+
+
+
+ + X + +
+
+
+ + + + + + + +
+
+
+ + Y + +
+
+
+
+ + Y + +
+
+
+ + + + + + + +
+
+
+ + Z + +
+
+
+
+ + Z + +
+
+
+ + + + + + + +
+
+
+ + Dictionary + +
+
+
+
+ + Dictionary + +
+
+
+ + + +
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/tutorial/indexes/dictionary003.svg b/docs/img/tutorial/indexes/dictionary003.svg deleted file mode 100644 index 0eafd5c561..0000000000 --- a/docs/img/tutorial/indexes/dictionary003.svg +++ /dev/null @@ -1 +0,0 @@ -
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
Viewer does not support full SVG 1.1
diff --git a/docs/img/tutorial/indexes/dictionary004.drawio b/docs/img/tutorial/indexes/dictionary004.drawio deleted file mode 100644 index 6c8590d6ea..0000000000 --- a/docs/img/tutorial/indexes/dictionary004.drawio +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/img/tutorial/indexes/dictionary004.drawio.svg b/docs/img/tutorial/indexes/dictionary004.drawio.svg new file mode 100644 index 0000000000..76974a4bfb --- /dev/null +++ b/docs/img/tutorial/indexes/dictionary004.drawio.svg @@ -0,0 +1,663 @@ + + + + + + + + + + + + + +
+
+
+ + A + +
+
+
+
+ + A + +
+
+
+ + + + + + + +
+
+
+ + B + +
+
+
+
+ + B + +
+
+
+ + + + + + + +
+
+
+ + C + +
+
+
+
+ + C + +
+
+
+ + + + + + + +
+
+
+ + D + +
+
+
+
+ + D + +
+
+
+ + + + + + + +
+
+
+ + E + +
+
+
+
+ + E + +
+
+
+ + + + + + + +
+
+
+ + F + +
+
+
+
+ + F + +
+
+
+ + + + + + + +
+
+
+ + G + +
+
+
+
+ + G + +
+
+
+ + + + + + + +
+
+
+ + H + +
+
+
+
+ + H + +
+
+
+ + + + + + + +
+
+
+ + I + +
+
+
+
+ + I + +
+
+
+ + + + + + + +
+
+
+ + J + +
+
+
+
+ + J + +
+
+
+ + + + + + + +
+
+
+ + K + +
+
+
+
+ + K + +
+
+
+ + + + + + + +
+
+
+ + L + +
+
+
+
+ + L + +
+
+
+ + + + + + + +
+
+
+ + M + +
+
+
+
+ + M + +
+
+
+ + + + + + + +
+
+
+ + N + +
+
+
+
+ + N + +
+
+
+ + + + + + + +
+
+
+ + O + +
+
+
+
+ + O + +
+
+
+ + + + + + + +
+
+
+ + P + +
+
+
+
+ + P + +
+
+
+ + + + + + + +
+
+
+ + Q + +
+
+
+
+ + Q + +
+
+
+ + + + + + + +
+
+
+ + R + +
+
+
+
+ + R + +
+
+
+ + + + + + + +
+
+
+ + S + +
+
+
+
+ + S + +
+
+
+ + + + + + + +
+
+
+ + T + +
+
+
+
+ + T + +
+
+
+ + + + + + + +
+
+
+ + U + +
+
+
+
+ + U + +
+
+
+ + + + + + + +
+
+
+ + V + +
+
+
+
+ + V + +
+
+
+ + + + + + + +
+
+
+ + W + +
+
+
+
+ + W + +
+
+
+ + + + + + + +
+
+
+ + X + +
+
+
+
+ + X + +
+
+
+ + + + + + + +
+
+
+ + Y + +
+
+
+
+ + Y + +
+
+
+ + + + + + + +
+
+
+ + Z + +
+
+
+
+ + Z + +
+
+
+ + + + + + + +
+
+
+ + Dictionary + +
+
+
+
+ + Dictionary + +
+
+
+ + + + + + + +
+
+
+ + F + +
+
+
+
+ + F + +
+
+
+ + + +
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/tutorial/indexes/dictionary004.svg b/docs/img/tutorial/indexes/dictionary004.svg deleted file mode 100644 index dcfcfc5a59..0000000000 --- a/docs/img/tutorial/indexes/dictionary004.svg +++ /dev/null @@ -1 +0,0 @@ -
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
F
F
Viewer does not support full SVG 1.1
diff --git a/docs/img/tutorial/indexes/dictionary005.drawio b/docs/img/tutorial/indexes/dictionary005.drawio deleted file mode 100644 index 33e21c6d90..0000000000 --- a/docs/img/tutorial/indexes/dictionary005.drawio +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/img/tutorial/indexes/dictionary005.drawio.svg b/docs/img/tutorial/indexes/dictionary005.drawio.svg new file mode 100644 index 0000000000..865ad3530f --- /dev/null +++ b/docs/img/tutorial/indexes/dictionary005.drawio.svg @@ -0,0 +1,640 @@ + + + + + + + + + + + + + +
+
+
+ + A + +
+
+
+
+ + A + +
+
+
+ + + + + + + +
+
+
+ + B + +
+
+
+
+ + B + +
+
+
+ + + + + + + +
+
+
+ + C + +
+
+
+
+ + C + +
+
+
+ + + + + + + +
+
+
+ + D + +
+
+
+
+ + D + +
+
+
+ + + + + + + +
+
+
+ + E + +
+
+
+
+ + E + +
+
+
+ + + + + + + +
+
+
+ + F + +
+
+
+
+ + F + +
+
+
+ + + + + + + +
+
+
+ + G + +
+
+
+
+ + G + +
+
+
+ + + + + + + +
+
+
+ + H + +
+
+
+
+ + H + +
+
+
+ + + + + + + +
+
+
+ + I + +
+
+
+
+ + I + +
+
+
+ + + + + + + +
+
+
+ + J + +
+
+
+
+ + J + +
+
+
+ + + + + + + +
+
+
+ + K + +
+
+
+
+ + K + +
+
+
+ + + + + + + +
+
+
+ + L + +
+
+
+
+ + L + +
+
+
+ + + + + + + +
+
+
+ + M + +
+
+
+
+ + M + +
+
+
+ + + + + + + +
+
+
+ + N + +
+
+
+
+ + N + +
+
+
+ + + + + + + +
+
+
+ + O + +
+
+
+
+ + O + +
+
+
+ + + + + + + +
+
+
+ + P + +
+
+
+
+ + P + +
+
+
+ + + + + + + +
+
+
+ + Q + +
+
+
+
+ + Q + +
+
+
+ + + + + + + +
+
+
+ + R + +
+
+
+
+ + R + +
+
+
+ + + + + + + +
+
+
+ + S + +
+
+
+
+ + S + +
+
+
+ + + + + + + +
+
+
+ + T + +
+
+
+
+ + T + +
+
+
+ + + + + + + +
+
+
+ + U + +
+
+
+
+ + U + +
+
+
+ + + + + + + +
+
+
+ + V + +
+
+
+
+ + V + +
+
+
+ + + + + + + +
+
+
+ + W + +
+
+
+
+ + W + +
+
+
+ + + + + + + +
+
+
+ + X + +
+
+
+
+ + X + +
+
+
+ + + + + + + +
+
+
+ + Y + +
+
+
+
+ + Y + +
+
+
+ + + + + + + +
+
+
+ + Z + +
+
+
+
+ + Z + +
+
+
+ + + + + + + +
+
+
+ + Dictionary + +
+
+
+
+ + Dictionary + +
+
+
+ + + +
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/tutorial/indexes/dictionary005.svg b/docs/img/tutorial/indexes/dictionary005.svg deleted file mode 100644 index 4bfa8c0ca4..0000000000 --- a/docs/img/tutorial/indexes/dictionary005.svg +++ /dev/null @@ -1 +0,0 @@ -
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
Viewer does not support full SVG 1.1
diff --git a/docs/img/tutorial/indexes/dictionary006.drawio b/docs/img/tutorial/indexes/dictionary006.drawio deleted file mode 100644 index 84366e87b0..0000000000 --- a/docs/img/tutorial/indexes/dictionary006.drawio +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/img/tutorial/indexes/dictionary006.drawio.svg b/docs/img/tutorial/indexes/dictionary006.drawio.svg new file mode 100644 index 0000000000..2947ee7db9 --- /dev/null +++ b/docs/img/tutorial/indexes/dictionary006.drawio.svg @@ -0,0 +1,663 @@ + + + + + + + + + + + + + +
+
+
+ + A + +
+
+
+
+ + A + +
+
+
+ + + + + + + +
+
+
+ + B + +
+
+
+
+ + B + +
+
+
+ + + + + + + +
+
+
+ + C + +
+
+
+
+ + C + +
+
+
+ + + + + + + +
+
+
+ + D + +
+
+
+
+ + D + +
+
+
+ + + + + + + +
+
+
+ + E + +
+
+
+
+ + E + +
+
+
+ + + + + + + +
+
+
+ + F + +
+
+
+
+ + F + +
+
+
+ + + + + + + +
+
+
+ + G + +
+
+
+
+ + G + +
+
+
+ + + + + + + +
+
+
+ + H + +
+
+
+
+ + H + +
+
+
+ + + + + + + +
+
+
+ + I + +
+
+
+
+ + I + +
+
+
+ + + + + + + +
+
+
+ + J + +
+
+
+
+ + J + +
+
+
+ + + + + + + +
+
+
+ + K + +
+
+
+
+ + K + +
+
+
+ + + + + + + +
+
+
+ + L + +
+
+
+
+ + L + +
+
+
+ + + + + + + +
+
+
+ + M + +
+
+
+
+ + M + +
+
+
+ + + + + + + +
+
+
+ + N + +
+
+
+
+ + N + +
+
+
+ + + + + + + +
+
+
+ + O + +
+
+
+
+ + O + +
+
+
+ + + + + + + +
+
+
+ + P + +
+
+
+
+ + P + +
+
+
+ + + + + + + +
+
+
+ + Q + +
+
+
+
+ + Q + +
+
+
+ + + + + + + +
+
+
+ + R + +
+
+
+
+ + R + +
+
+
+ + + + + + + +
+
+
+ + S + +
+
+
+
+ + S + +
+
+
+ + + + + + + +
+
+
+ + T + +
+
+
+
+ + T + +
+
+
+ + + + + + + +
+
+
+ + U + +
+
+
+
+ + U + +
+
+
+ + + + + + + +
+
+
+ + V + +
+
+
+
+ + V + +
+
+
+ + + + + + + +
+
+
+ + W + +
+
+
+
+ + W + +
+
+
+ + + + + + + +
+
+
+ + X + +
+
+
+
+ + X + +
+
+
+ + + + + + + +
+
+
+ + Y + +
+
+
+
+ + Y + +
+
+
+ + + + + + + +
+
+
+ + Z + +
+
+
+
+ + Z + +
+
+
+ + + + + + + +
+
+
+ + Dictionary + +
+
+
+
+ + Dictionary + +
+
+
+ + + + + + + + + + +
+
+
+ + C + +
+
+
+
+ + C + +
+
+
+
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/tutorial/indexes/dictionary006.svg b/docs/img/tutorial/indexes/dictionary006.svg deleted file mode 100644 index f893ca4cd4..0000000000 --- a/docs/img/tutorial/indexes/dictionary006.svg +++ /dev/null @@ -1 +0,0 @@ -
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
C
C
Viewer does not support full SVG 1.1
diff --git a/docs/img/tutorial/indexes/dictionary007.drawio b/docs/img/tutorial/indexes/dictionary007.drawio deleted file mode 100644 index 62ad844200..0000000000 --- a/docs/img/tutorial/indexes/dictionary007.drawio +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/img/tutorial/indexes/dictionary007.drawio.svg b/docs/img/tutorial/indexes/dictionary007.drawio.svg new file mode 100644 index 0000000000..651ad3287c --- /dev/null +++ b/docs/img/tutorial/indexes/dictionary007.drawio.svg @@ -0,0 +1,643 @@ + + + + + + + + + + + + + +
+
+
+ + A + +
+
+
+
+ + A + +
+
+
+ + + + + + + +
+
+
+ + B + +
+
+
+
+ + B + +
+
+
+ + + + + + + +
+
+
+ + C + +
+
+
+
+ + C + +
+
+
+ + + + + + + +
+
+
+ + D + +
+
+
+
+ + D + +
+
+
+ + + + + + + +
+
+
+ + E + +
+
+
+
+ + E + +
+
+
+ + + + + + + +
+
+
+ + F + +
+
+
+
+ + F + +
+
+
+ + + + + + + +
+
+
+ + G + +
+
+
+
+ + G + +
+
+
+ + + + + + + +
+
+
+ + H + +
+
+
+
+ + H + +
+
+
+ + + + + + + +
+
+
+ + I + +
+
+
+
+ + I + +
+
+
+ + + + + + + +
+
+
+ + J + +
+
+
+
+ + J + +
+
+
+ + + + + + + +
+
+
+ + K + +
+
+
+
+ + K + +
+
+
+ + + + + + + +
+
+
+ + L + +
+
+
+
+ + L + +
+
+
+ + + + + + + +
+
+
+ + M + +
+
+
+
+ + M + +
+
+
+ + + + + + + +
+
+
+ + N + +
+
+
+
+ + N + +
+
+
+ + + + + + + +
+
+
+ + O + +
+
+
+
+ + O + +
+
+
+ + + + + + + +
+
+
+ + P + +
+
+
+
+ + P + +
+
+
+ + + + + + + +
+
+
+ + Q + +
+
+
+
+ + Q + +
+
+
+ + + + + + + +
+
+
+ + R + +
+
+
+
+ + R + +
+
+
+ + + + + + + +
+
+
+ + S + +
+
+
+
+ + S + +
+
+
+ + + + + + + +
+
+
+ + T + +
+
+
+
+ + T + +
+
+
+ + + + + + + +
+
+
+ + U + +
+
+
+
+ + U + +
+
+
+ + + + + + + +
+
+
+ + V + +
+
+
+
+ + V + +
+
+
+ + + + + + + +
+
+
+ + W + +
+
+
+
+ + W + +
+
+
+ + + + + + + +
+
+
+ + X + +
+
+
+
+ + X + +
+
+
+ + + + + + + +
+
+
+ + Y + +
+
+
+
+ + Y + +
+
+
+ + + + + + + +
+
+
+ + Z + +
+
+
+
+ + Z + +
+
+
+ + + + + + + +
+
+
+ + Dictionary + +
+
+
+
+ + Dictionary + +
+
+
+ + + + + + +
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/tutorial/indexes/dictionary007.svg b/docs/img/tutorial/indexes/dictionary007.svg deleted file mode 100644 index 8f71aa8311..0000000000 --- a/docs/img/tutorial/indexes/dictionary007.svg +++ /dev/null @@ -1 +0,0 @@ -
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
Viewer does not support full SVG 1.1
diff --git a/docs/img/tutorial/indexes/dictionary008.drawio b/docs/img/tutorial/indexes/dictionary008.drawio deleted file mode 100644 index 30a1f66277..0000000000 --- a/docs/img/tutorial/indexes/dictionary008.drawio +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/img/tutorial/indexes/dictionary008.drawio.svg b/docs/img/tutorial/indexes/dictionary008.drawio.svg new file mode 100644 index 0000000000..611c33177b --- /dev/null +++ b/docs/img/tutorial/indexes/dictionary008.drawio.svg @@ -0,0 +1,666 @@ + + + + + + + + + + + + + +
+
+
+ + A + +
+
+
+
+ + A + +
+
+
+ + + + + + + +
+
+
+ + B + +
+
+
+
+ + B + +
+
+
+ + + + + + + +
+
+
+ + C + +
+
+
+
+ + C + +
+
+
+ + + + + + + +
+
+
+ + D + +
+
+
+
+ + D + +
+
+
+ + + + + + + +
+
+
+ + E + +
+
+
+
+ + E + +
+
+
+ + + + + + + +
+
+
+ + F + +
+
+
+
+ + F + +
+
+
+ + + + + + + +
+
+
+ + G + +
+
+
+
+ + G + +
+
+
+ + + + + + + +
+
+
+ + H + +
+
+
+
+ + H + +
+
+
+ + + + + + + +
+
+
+ + I + +
+
+
+
+ + I + +
+
+
+ + + + + + + +
+
+
+ + J + +
+
+
+
+ + J + +
+
+
+ + + + + + + +
+
+
+ + K + +
+
+
+
+ + K + +
+
+
+ + + + + + + +
+
+
+ + L + +
+
+
+
+ + L + +
+
+
+ + + + + + + +
+
+
+ + M + +
+
+
+
+ + M + +
+
+
+ + + + + + + +
+
+
+ + N + +
+
+
+
+ + N + +
+
+
+ + + + + + + +
+
+
+ + O + +
+
+
+
+ + O + +
+
+
+ + + + + + + +
+
+
+ + P + +
+
+
+
+ + P + +
+
+
+ + + + + + + +
+
+
+ + Q + +
+
+
+
+ + Q + +
+
+
+ + + + + + + +
+
+
+ + R + +
+
+
+
+ + R + +
+
+
+ + + + + + + +
+
+
+ + S + +
+
+
+
+ + S + +
+
+
+ + + + + + + +
+
+
+ + T + +
+
+
+
+ + T + +
+
+
+ + + + + + + +
+
+
+ + U + +
+
+
+
+ + U + +
+
+
+ + + + + + + +
+
+
+ + V + +
+
+
+
+ + V + +
+
+
+ + + + + + + +
+
+
+ + W + +
+
+
+
+ + W + +
+
+
+ + + + + + + +
+
+
+ + X + +
+
+
+
+ + X + +
+
+
+ + + + + + + +
+
+
+ + Y + +
+
+
+
+ + Y + +
+
+
+ + + + + + + +
+
+
+ + Z + +
+
+
+
+ + Z + +
+
+
+ + + + + + + +
+
+
+ + Dictionary + +
+
+
+
+ + Dictionary + +
+
+
+ + + + + + + + + + + + + +
+
+
+ + D + +
+
+
+
+ + D + +
+
+
+
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/tutorial/indexes/dictionary008.svg b/docs/img/tutorial/indexes/dictionary008.svg deleted file mode 100644 index 5a48e18a20..0000000000 --- a/docs/img/tutorial/indexes/dictionary008.svg +++ /dev/null @@ -1 +0,0 @@ -
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
N
N
O
O
P
P
Q
Q
R
R
S
S
T
T
U
U
V
V
W
W
X
X
Y
Y
Z
Z
Dictionary
Dictionary
D
D
Viewer does not support full SVG 1.1
diff --git a/docs/img/tutorial/indexes/techbook001.drawio b/docs/img/tutorial/indexes/techbook001.drawio deleted file mode 100644 index 5fffe3c3ea..0000000000 --- a/docs/img/tutorial/indexes/techbook001.drawio +++ /dev/null @@ -1,92 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/img/tutorial/indexes/techbook001.drawio.svg b/docs/img/tutorial/indexes/techbook001.drawio.svg new file mode 100644 index 0000000000..cb022465f6 --- /dev/null +++ b/docs/img/tutorial/indexes/techbook001.drawio.svg @@ -0,0 +1,346 @@ + + + + + + + + + + + + + +
+
+
+ + Technical Book + +
+
+
+
+ + Technical Book + +
+
+
+ + + + + + + +
+
+
+ + Chapter 1 + +
+
+
+
+ + Chapter 1 + +
+
+
+ + + + + + + +
+
+
+ + Chapter 2 + +
+
+
+
+ + Chapter 2 + +
+
+
+ + + + + + + +
+
+
+ + Chapter 3 + +
+
+
+
+ + Chapter 3 + +
+
+
+ + + + + + + +
+
+
+ + Chapter 4 + +
+
+
+
+ + Chapter 4 + +
+
+
+ + + + + + + +
+
+
+ + Chapter 5 + +
+
+
+
+ + Chapter 5 + +
+
+
+ + + + + + + +
+
+
+ + Chapter 6 + +
+
+
+
+ + Chapter 6 + +
+
+
+ + + + + + + +
+
+
+ + Chapter 7 + +
+
+
+
+ + Chapter 7 + +
+
+
+ + + + + + + + + + +
+
+
+ + Book Index + +
+
+
+
+ + Book Index + +
+
+
+ + + + + + + + + + + + + + + +
+
+
+ + Database + +
+
+
+
+ + Database + +
+
+
+ + + + + + + + + + + + + + + +
+
+
+ + Python + +
+
+
+
+ + Python + +
+
+
+ + + + + + + + + + + + + + + +
+
+
+ + Files + +
+
+
+
+ + Files + +
+
+
+ + + + + + + + + + + +
+
+
+ + Editors + +
+
+
+
+ + Editors + +
+
+
+
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/tutorial/indexes/techbook001.svg b/docs/img/tutorial/indexes/techbook001.svg deleted file mode 100644 index 3f44f50742..0000000000 --- a/docs/img/tutorial/indexes/techbook001.svg +++ /dev/null @@ -1 +0,0 @@ -
Technical Book
Technical Book
Chapter 1
Chapter 1
Chapter 2
Chapter 2
Chapter 3
Chapter 3
Chapter 4
Chapter 4
Chapter 5
Chapter 5
Chapter 6
Chapter 6
Chapter 7
Chapter 7
Book Index
Book Index
Database
Database
Python
Python
Files
Files
Editors
Editors
Viewer does not support full SVG 1.1
diff --git a/docs/img/tutorial/many-to-many/many-to-many.drawio b/docs/img/tutorial/many-to-many/many-to-many.drawio deleted file mode 100644 index b33e547fc8..0000000000 --- a/docs/img/tutorial/many-to-many/many-to-many.drawio +++ /dev/null @@ -1,220 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/img/tutorial/many-to-many/many-to-many.drawio.svg b/docs/img/tutorial/many-to-many/many-to-many.drawio.svg new file mode 100644 index 0000000000..d362455cec --- /dev/null +++ b/docs/img/tutorial/many-to-many/many-to-many.drawio.svg @@ -0,0 +1,1206 @@ + + + + + + + + + + + + + + + + + + + + +
+
+
+ + hero + +
+
+
+
+ + hero + +
+
+
+ + + + + + + + + + + +
+
+
+ + + id + + +
+
+
+
+ + id + +
+
+
+ + + + + + + + +
+
+
+ + + name + + +
+
+
+
+ + name + +
+
+
+ + + + + + + + +
+
+
+ + + secret_name + + +
+
+
+
+ + secret_name + +
+
+
+ + + + + + + + +
+
+
+ + + age + + +
+
+
+
+ + age + +
+
+
+ + + + + + + + + + + +
+
+
+ + + 1 + + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + +
+
+
+ + Deadpond + +
+
+
+
+ + Deadpond + +
+
+
+ + + + + + + + +
+
+
+ + Dive Wilson + +
+
+
+
+ + Dive Wilson + +
+
+
+ + + + + + + + +
+
+
+ + null + +
+
+
+
+ + null + +
+
+
+ + + + + + + + + + + +
+
+
+ + 2 + +
+
+
+
+ + 2 + +
+
+
+ + + + + + + + +
+
+
+ + Spider-Boy + +
+
+
+
+ + Spider-Boy + +
+
+
+ + + + + + + + +
+
+
+ + Pedro Parqueador + +
+
+
+
+ + Pedro Parqueador + +
+
+
+ + + + + + + + +
+
+
+ + null + +
+
+
+
+ + null + +
+
+
+ + + + + + + + + + + +
+
+
+ + 3 + +
+
+
+
+ + 3 + +
+
+
+ + + + + + + + +
+
+
+ + Rusty-Man + +
+
+
+
+ + Rusty-Man + +
+
+
+ + + + + + + + +
+
+
+ + Tommy Sharp + +
+
+
+
+ + Tommy Sharp + +
+
+
+ + + + + + + + +
+
+
+ + 48 + +
+
+
+
+ + 48 + +
+
+
+ + + + + + + + + + + + + +
+
+
+ + team + +
+
+
+
+ + team + +
+
+
+ + + + + + + + + + + +
+
+
+ + + id + + +
+
+
+
+ + id + +
+
+
+ + + + + + + + +
+
+
+ + + name + + +
+
+
+
+ + name + +
+
+
+ + + + + + + + +
+
+
+ + + headquarters + + +
+
+
+
+ + headquarters + +
+
+
+ + + + + + + + + + + +
+
+
+ + + 1 + + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + +
+
+
+ + Preventers + +
+
+
+
+ + Preventers + +
+
+
+ + + + + + + + +
+
+
+ + + Sharp Tower + + +
+
+
+
+ + Sharp Tower + +
+
+
+ + + + + + + + + + + +
+
+
+ + 2 + +
+
+
+
+ + 2 + +
+
+
+ + + + + + + + +
+
+
+ + Z-Force + +
+
+
+
+ + Z-Force + +
+
+
+ + + + + + + + +
+
+
+

+ + Sister Margaret's Bar + +

+
+
+
+
+ + Sister Margaret's Bar + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + heroteamlink + +
+
+
+
+ + heroteamlink + +
+
+
+ + + + + + + + + + + +
+
+
+ + + hero_id + + +
+
+
+
+ + hero_id + +
+
+
+ + + + + + + + +
+
+
+ + + team_id + + +
+
+
+
+ + team_id + +
+
+
+ + + + + + + + + + + +
+
+
+ + + 1 + + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + +
+
+
+ + 1 + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + + + + +
+
+
+ + 1 + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + +
+
+
+ + 2 + +
+
+
+
+ + 2 + +
+
+
+ + + + + + + + + + + +
+
+
+ + 2 + +
+
+
+
+ + 2 + +
+
+
+ + + + + + + + +
+
+
+ + 1 + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + + + + +
+
+
+ + 3 + +
+
+
+
+ + 3 + +
+
+
+ + + + + + + + +
+
+
+ + + 1 + + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/tutorial/many-to-many/many-to-many.svg b/docs/img/tutorial/many-to-many/many-to-many.svg deleted file mode 100644 index 847a8351f0..0000000000 --- a/docs/img/tutorial/many-to-many/many-to-many.svg +++ /dev/null @@ -1,57 +0,0 @@ -
hero
hero
id
id
name
name
secret_name
secret_name
age
age
1
1
Deadpond
Deadpond
Dive Wilson
Dive Wilson
null
null
2
2
Spider-Boy
Spider-Boy
Pedro Parqueador
Pedro Parqueador
null
null
3
3
Rusty-Man
Rusty-Man
Tommy Sharp
Tommy Sharp
48
48
team
team
id
id
name
name
headquarters
headquarters
1
1
Preventers
Preventers
Sharp Tower
Sharp Tower
2
2
Z-Force
Z-Force

Sister Margaret's Bar

Sister Margaret's Bar
heroteamlink
heroteamlink
hero_id
hero_id
team_id
team_id
1
1
1
1
1
1
2
2
2
2
1
1
3
3
1
1
Viewer does not support full SVG 1.1
diff --git a/docs/img/tutorial/offset-and-limit/limit.drawio b/docs/img/tutorial/offset-and-limit/limit.drawio deleted file mode 100644 index da7f17ce99..0000000000 --- a/docs/img/tutorial/offset-and-limit/limit.drawio +++ /dev/null @@ -1,133 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/img/tutorial/offset-and-limit/limit.drawio.svg b/docs/img/tutorial/offset-and-limit/limit.drawio.svg new file mode 100644 index 0000000000..41ec69a3cd --- /dev/null +++ b/docs/img/tutorial/offset-and-limit/limit.drawio.svg @@ -0,0 +1,1031 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + hero + +
+
+
+
+ + hero + +
+
+
+ + + + + + + + + + + +
+
+
+ + + id + + +
+
+
+
+ + id + +
+
+
+ + + + + + + + +
+
+
+ + + name + + +
+
+
+
+ + name + +
+
+
+ + + + + + + + +
+
+
+ + + secret_name + + +
+
+
+
+ + secret_name + +
+
+
+ + + + + + + + +
+
+
+ + + age + + +
+
+
+
+ + age + +
+
+
+ + + + + + + + + + + +
+
+
+ + + 1 + + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + +
+
+
+ + Deadpond + +
+
+
+
+ + Deadpond + +
+
+
+ + + + + + + + +
+
+
+ + Dive Wilson + +
+
+
+
+ + Dive Wilson + +
+
+
+ + + + + + + + +
+
+
+ + null + +
+
+
+
+ + null + +
+
+
+ + + + + + + + + + + +
+
+
+ + 2 + +
+
+
+
+ + 2 + +
+
+
+ + + + + + + + +
+
+
+ + Spider-Boy + +
+
+
+
+ + Spider-Boy + +
+
+
+ + + + + + + + +
+
+
+ + Pedro Parqueador + +
+
+
+
+ + Pedro Parqueador + +
+
+
+ + + + + + + + +
+
+
+ + null + +
+
+
+
+ + null + +
+
+
+ + + + + + + + + + + +
+
+
+ + 3 + +
+
+
+
+ + 3 + +
+
+
+ + + + + + + + +
+
+
+ + Rusty-Man + +
+
+
+
+ + Rusty-Man + +
+
+
+ + + + + + + + +
+
+
+ + Tommy Sharp + +
+
+
+
+ + Tommy Sharp + +
+
+
+ + + + + + + + +
+
+
+ + 48 + +
+
+
+
+ + 48 + +
+
+
+ + + + + + + + + + + +
+
+
+ + + 4 + + +
+
+
+
+ + 4 + +
+
+
+ + + + + + + + +
+
+
+ + Tarantula + +
+
+
+
+ + Tarantula + +
+
+
+ + + + + + + + +
+
+
+ + + Natalia Roman-on + + +
+
+
+
+ + Natalia Roman-on + +
+
+
+ + + + + + + + +
+
+
+ + 32 + +
+
+
+
+ + 32 + +
+
+
+ + + + + + + + + + + +
+
+
+ + 5 + +
+
+
+
+ + 5 + +
+
+
+ + + + + + + + +
+
+
+ + + Black Lion + + +
+
+
+
+ + Black Lion + +
+
+
+ + + + + + + + +
+
+
+ + + Trevor Challa + + +
+
+
+
+ + Trevor Challa + +
+
+
+ + + + + + + + +
+
+
+ + 35 + +
+
+
+
+ + 35 + +
+
+
+ + + + + + + + + + + +
+
+
+ + 6 + +
+
+
+
+ + 6 + +
+
+
+ + + + + + + + +
+
+
+ + + Dr. Weird + + +
+
+
+
+ + Dr. Weird + +
+
+
+ + + + + + + + +
+
+
+ + + Steve Weird + + +
+
+
+
+ + Steve Weird + +
+
+
+ + + + + + + + +
+
+
+ + 36 + +
+
+
+
+ + 36 + +
+
+
+ + + + + + + + + + + +
+
+
+ + 7 + +
+
+
+
+ + 7 + +
+
+
+ + + + + + + + +
+
+
+ + + Captain North America + + +
+
+
+
+ + Captain North America + +
+
+
+ + + + + + + + +
+
+
+ + + Esteban Rogelios + + +
+
+
+
+ + Esteban Rogelios + +
+
+
+ + + + + + + + +
+
+
+ + 93 + +
+
+
+
+ + 93 + +
+
+
+
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/tutorial/offset-and-limit/limit.svg b/docs/img/tutorial/offset-and-limit/limit.svg deleted file mode 100644 index d05669e3c5..0000000000 --- a/docs/img/tutorial/offset-and-limit/limit.svg +++ /dev/null @@ -1,57 +0,0 @@ -
hero
hero
id
id
name
name
secret_name
secret_name
age
age
1
1
Deadpond
Deadpond
Dive Wilson
Dive Wilson
null
null
2
2
Spider-Boy
Spider-Boy
Pedro Parqueador
Pedro Parqueador
null
null
3
3
Rusty-Man
Rusty-Man
Tommy Sharp
Tommy Sharp
48
48
4
4
Tarantula
Tarantula
Natalia Roman-on
Natalia Roman-on
32
32
5
5
Black Lion
Black Lion
Trevor Challa
Trevor Challa
35
35
6
6
Dr. Weird
Dr. Weird
Steve Weird
Steve Weird
36
36
7
7
Captain North America
Captain North America
Esteban Rogelios
Esteban Rogelios
93
93
Viewer does not support full SVG 1.1
diff --git a/docs/img/tutorial/offset-and-limit/limit2.drawio b/docs/img/tutorial/offset-and-limit/limit2.drawio deleted file mode 100644 index 68f9817052..0000000000 --- a/docs/img/tutorial/offset-and-limit/limit2.drawio +++ /dev/null @@ -1,133 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/img/tutorial/offset-and-limit/limit2.drawio.svg b/docs/img/tutorial/offset-and-limit/limit2.drawio.svg new file mode 100644 index 0000000000..1f23e8682b --- /dev/null +++ b/docs/img/tutorial/offset-and-limit/limit2.drawio.svg @@ -0,0 +1,1031 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + hero + +
+
+
+
+ + hero + +
+
+
+ + + + + + + + + + + +
+
+
+ + + id + + +
+
+
+
+ + id + +
+
+
+ + + + + + + + +
+
+
+ + + name + + +
+
+
+
+ + name + +
+
+
+ + + + + + + + +
+
+
+ + + secret_name + + +
+
+
+
+ + secret_name + +
+
+
+ + + + + + + + +
+
+
+ + + age + + +
+
+
+
+ + age + +
+
+
+ + + + + + + + + + + +
+
+
+ + + 1 + + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + +
+
+
+ + Deadpond + +
+
+
+
+ + Deadpond + +
+
+
+ + + + + + + + +
+
+
+ + Dive Wilson + +
+
+
+
+ + Dive Wilson + +
+
+
+ + + + + + + + +
+
+
+ + null + +
+
+
+
+ + null + +
+
+
+ + + + + + + + + + + +
+
+
+ + 2 + +
+
+
+
+ + 2 + +
+
+
+ + + + + + + + +
+
+
+ + Spider-Boy + +
+
+
+
+ + Spider-Boy + +
+
+
+ + + + + + + + +
+
+
+ + Pedro Parqueador + +
+
+
+
+ + Pedro Parqueador + +
+
+
+ + + + + + + + +
+
+
+ + null + +
+
+
+
+ + null + +
+
+
+ + + + + + + + + + + +
+
+
+ + 3 + +
+
+
+
+ + 3 + +
+
+
+ + + + + + + + +
+
+
+ + Rusty-Man + +
+
+
+
+ + Rusty-Man + +
+
+
+ + + + + + + + +
+
+
+ + Tommy Sharp + +
+
+
+
+ + Tommy Sharp + +
+
+
+ + + + + + + + +
+
+
+ + 48 + +
+
+
+
+ + 48 + +
+
+
+ + + + + + + + + + + +
+
+
+ + + 4 + + +
+
+
+
+ + 4 + +
+
+
+ + + + + + + + +
+
+
+ + Tarantula + +
+
+
+
+ + Tarantula + +
+
+
+ + + + + + + + +
+
+
+ + + Natalia Roman-on + + +
+
+
+
+ + Natalia Roman-on + +
+
+
+ + + + + + + + +
+
+
+ + 32 + +
+
+
+
+ + 32 + +
+
+
+ + + + + + + + + + + +
+
+
+ + 5 + +
+
+
+
+ + 5 + +
+
+
+ + + + + + + + +
+
+
+ + + Black Lion + + +
+
+
+
+ + Black Lion + +
+
+
+ + + + + + + + +
+
+
+ + + Trevor Challa + + +
+
+
+
+ + Trevor Challa + +
+
+
+ + + + + + + + +
+
+
+ + 35 + +
+
+
+
+ + 35 + +
+
+
+ + + + + + + + + + + +
+
+
+ + 6 + +
+
+
+
+ + 6 + +
+
+
+ + + + + + + + +
+
+
+ + + Dr. Weird + + +
+
+
+
+ + Dr. Weird + +
+
+
+ + + + + + + + +
+
+
+ + + Steve Weird + + +
+
+
+
+ + Steve Weird + +
+
+
+ + + + + + + + +
+
+
+ + 36 + +
+
+
+
+ + 36 + +
+
+
+ + + + + + + + + + + +
+
+
+ + 7 + +
+
+
+
+ + 7 + +
+
+
+ + + + + + + + +
+
+
+ + + Captain North America + + +
+
+
+
+ + Captain North America + +
+
+
+ + + + + + + + +
+
+
+ + + Esteban Rogelios + + +
+
+
+
+ + Esteban Rogelios + +
+
+
+ + + + + + + + +
+
+
+ + 93 + +
+
+
+
+ + 93 + +
+
+
+
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/tutorial/offset-and-limit/limit2.svg b/docs/img/tutorial/offset-and-limit/limit2.svg deleted file mode 100644 index f574f13640..0000000000 --- a/docs/img/tutorial/offset-and-limit/limit2.svg +++ /dev/null @@ -1,57 +0,0 @@ -
hero
hero
id
id
name
name
secret_name
secret_name
age
age
1
1
Deadpond
Deadpond
Dive Wilson
Dive Wilson
null
null
2
2
Spider-Boy
Spider-Boy
Pedro Parqueador
Pedro Parqueador
null
null
3
3
Rusty-Man
Rusty-Man
Tommy Sharp
Tommy Sharp
48
48
4
4
Tarantula
Tarantula
Natalia Roman-on
Natalia Roman-on
32
32
5
5
Black Lion
Black Lion
Trevor Challa
Trevor Challa
35
35
6
6
Dr. Weird
Dr. Weird
Steve Weird
Steve Weird
36
36
7
7
Captain North America
Captain North America
Esteban Rogelios
Esteban Rogelios
93
93
Viewer does not support full SVG 1.1
diff --git a/docs/img/tutorial/offset-and-limit/limit3.drawio b/docs/img/tutorial/offset-and-limit/limit3.drawio deleted file mode 100644 index 93e4c2df53..0000000000 --- a/docs/img/tutorial/offset-and-limit/limit3.drawio +++ /dev/null @@ -1,133 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/img/tutorial/offset-and-limit/limit3.drawio.svg b/docs/img/tutorial/offset-and-limit/limit3.drawio.svg new file mode 100644 index 0000000000..30b1c9f85a --- /dev/null +++ b/docs/img/tutorial/offset-and-limit/limit3.drawio.svg @@ -0,0 +1,1031 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + hero + +
+
+
+
+ + hero + +
+
+
+ + + + + + + + + + + +
+
+
+ + + id + + +
+
+
+
+ + id + +
+
+
+ + + + + + + + +
+
+
+ + + name + + +
+
+
+
+ + name + +
+
+
+ + + + + + + + +
+
+
+ + + secret_name + + +
+
+
+
+ + secret_name + +
+
+
+ + + + + + + + +
+
+
+ + + age + + +
+
+
+
+ + age + +
+
+
+ + + + + + + + + + + +
+
+
+ + + 1 + + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + +
+
+
+ + Deadpond + +
+
+
+
+ + Deadpond + +
+
+
+ + + + + + + + +
+
+
+ + Dive Wilson + +
+
+
+
+ + Dive Wilson + +
+
+
+ + + + + + + + +
+
+
+ + null + +
+
+
+
+ + null + +
+
+
+ + + + + + + + + + + +
+
+
+ + 2 + +
+
+
+
+ + 2 + +
+
+
+ + + + + + + + +
+
+
+ + Spider-Boy + +
+
+
+
+ + Spider-Boy + +
+
+
+ + + + + + + + +
+
+
+ + Pedro Parqueador + +
+
+
+
+ + Pedro Parqueador + +
+
+
+ + + + + + + + +
+
+
+ + null + +
+
+
+
+ + null + +
+
+
+ + + + + + + + + + + +
+
+
+ + 3 + +
+
+
+
+ + 3 + +
+
+
+ + + + + + + + +
+
+
+ + Rusty-Man + +
+
+
+
+ + Rusty-Man + +
+
+
+ + + + + + + + +
+
+
+ + Tommy Sharp + +
+
+
+
+ + Tommy Sharp + +
+
+
+ + + + + + + + +
+
+
+ + 48 + +
+
+
+
+ + 48 + +
+
+
+ + + + + + + + + + + +
+
+
+ + + 4 + + +
+
+
+
+ + 4 + +
+
+
+ + + + + + + + +
+
+
+ + Tarantula + +
+
+
+
+ + Tarantula + +
+
+
+ + + + + + + + +
+
+
+ + + Natalia Roman-on + + +
+
+
+
+ + Natalia Roman-on + +
+
+
+ + + + + + + + +
+
+
+ + 32 + +
+
+
+
+ + 32 + +
+
+
+ + + + + + + + + + + +
+
+
+ + 5 + +
+
+
+
+ + 5 + +
+
+
+ + + + + + + + +
+
+
+ + + Black Lion + + +
+
+
+
+ + Black Lion + +
+
+
+ + + + + + + + +
+
+
+ + + Trevor Challa + + +
+
+
+
+ + Trevor Challa + +
+
+
+ + + + + + + + +
+
+
+ + 35 + +
+
+
+
+ + 35 + +
+
+
+ + + + + + + + + + + +
+
+
+ + 6 + +
+
+
+
+ + 6 + +
+
+
+ + + + + + + + +
+
+
+ + + Dr. Weird + + +
+
+
+
+ + Dr. Weird + +
+
+
+ + + + + + + + +
+
+
+ + + Steve Weird + + +
+
+
+
+ + Steve Weird + +
+
+
+ + + + + + + + +
+
+
+ + 36 + +
+
+
+
+ + 36 + +
+
+
+ + + + + + + + + + + +
+
+
+ + 7 + +
+
+
+
+ + 7 + +
+
+
+ + + + + + + + +
+
+
+ + + Captain North America + + +
+
+
+
+ + Captain North America + +
+
+
+ + + + + + + + +
+
+
+ + + Esteban Rogelios + + +
+
+
+
+ + Esteban Rogelios + +
+
+
+ + + + + + + + +
+
+
+ + 93 + +
+
+
+
+ + 93 + +
+
+
+
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/tutorial/offset-and-limit/limit3.svg b/docs/img/tutorial/offset-and-limit/limit3.svg deleted file mode 100644 index 8a1a560b00..0000000000 --- a/docs/img/tutorial/offset-and-limit/limit3.svg +++ /dev/null @@ -1,57 +0,0 @@ -
hero
hero
id
id
name
name
secret_name
secret_name
age
age
1
1
Deadpond
Deadpond
Dive Wilson
Dive Wilson
null
null
2
2
Spider-Boy
Spider-Boy
Pedro Parqueador
Pedro Parqueador
null
null
3
3
Rusty-Man
Rusty-Man
Tommy Sharp
Tommy Sharp
48
48
4
4
Tarantula
Tarantula
Natalia Roman-on
Natalia Roman-on
32
32
5
5
Black Lion
Black Lion
Trevor Challa
Trevor Challa
35
35
6
6
Dr. Weird
Dr. Weird
Steve Weird
Steve Weird
36
36
7
7
Captain North America
Captain North America
Esteban Rogelios
Esteban Rogelios
93
93
Viewer does not support full SVG 1.1
diff --git a/docs/img/tutorial/relationships/attributes/back-populates.drawio b/docs/img/tutorial/relationships/attributes/back-populates.drawio deleted file mode 100644 index 5fc8c6fa73..0000000000 --- a/docs/img/tutorial/relationships/attributes/back-populates.drawio +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/img/tutorial/relationships/attributes/back-populates.drawio.svg b/docs/img/tutorial/relationships/attributes/back-populates.drawio.svg new file mode 100644 index 0000000000..434eab1c32 --- /dev/null +++ b/docs/img/tutorial/relationships/attributes/back-populates.drawio.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/docs/img/tutorial/relationships/attributes/back-populates.svg b/docs/img/tutorial/relationships/attributes/back-populates.svg deleted file mode 100644 index 631f73c808..0000000000 --- a/docs/img/tutorial/relationships/attributes/back-populates.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docs/img/tutorial/relationships/attributes/back-populates2.drawio b/docs/img/tutorial/relationships/attributes/back-populates2.drawio deleted file mode 100644 index c5d283feb0..0000000000 --- a/docs/img/tutorial/relationships/attributes/back-populates2.drawio +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/img/tutorial/relationships/attributes/back-populates2.drawio.svg b/docs/img/tutorial/relationships/attributes/back-populates2.drawio.svg new file mode 100644 index 0000000000..b94723dc19 --- /dev/null +++ b/docs/img/tutorial/relationships/attributes/back-populates2.drawio.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/docs/img/tutorial/relationships/attributes/back-populates2.svg b/docs/img/tutorial/relationships/attributes/back-populates2.svg deleted file mode 100644 index 929cb0a042..0000000000 --- a/docs/img/tutorial/relationships/attributes/back-populates2.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docs/img/tutorial/relationships/select/relationships2.drawio b/docs/img/tutorial/relationships/select/relationships2.drawio deleted file mode 100644 index e3f25a203e..0000000000 --- a/docs/img/tutorial/relationships/select/relationships2.drawio +++ /dev/null @@ -1,142 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/img/tutorial/relationships/select/relationships2.drawio.svg b/docs/img/tutorial/relationships/select/relationships2.drawio.svg new file mode 100644 index 0000000000..131c74dc15 --- /dev/null +++ b/docs/img/tutorial/relationships/select/relationships2.drawio.svg @@ -0,0 +1,988 @@ + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + hero + +
+
+
+
+ + hero + +
+
+
+ + + + + + + + + + + +
+
+
+ + + id + + +
+
+
+
+ + id + +
+
+
+ + + + + + + + +
+
+
+ + + name + + +
+
+
+
+ + name + +
+
+
+ + + + + + + + +
+
+
+ + + secret_name + + +
+
+
+
+ + secret_name + +
+
+
+ + + + + + + + +
+
+
+ + + age + + +
+
+
+
+ + age + +
+
+
+ + + + + + + + +
+
+
+ + + team_id + + +
+
+
+
+ + team_id + +
+
+
+ + + + + + + + + + + +
+
+
+ + + 1 + + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + +
+
+
+ + Deadpond + +
+
+
+
+ + Deadpond + +
+
+
+ + + + + + + + +
+
+
+ + Dive Wilson + +
+
+
+
+ + Dive Wilson + +
+
+
+ + + + + + + + +
+
+
+ + null + +
+
+
+
+ + null + +
+
+
+ + + + + + + + +
+
+
+ + 2 + +
+
+
+
+ + 2 + +
+
+
+ + + + + + + + + + + +
+
+
+ + 2 + +
+
+
+
+ + 2 + +
+
+
+ + + + + + + + +
+
+
+ + Spider-Boy + +
+
+
+
+ + Spider-Boy + +
+
+
+ + + + + + + + +
+
+
+ + Pedro Parqueador + +
+
+
+
+ + Pedro Parqueador + +
+
+
+ + + + + + + + +
+
+
+ + null + +
+
+
+
+ + null + +
+
+
+ + + + + + + + +
+
+
+ + null + +
+
+
+
+ + null + +
+
+
+ + + + + + + + + + + +
+
+
+ + 3 + +
+
+
+
+ + 3 + +
+
+
+ + + + + + + + +
+
+
+ + Rusty-Man + +
+
+
+
+ + Rusty-Man + +
+
+
+ + + + + + + + +
+
+
+ + Tommy Sharp + +
+
+
+
+ + Tommy Sharp + +
+
+
+ + + + + + + + +
+
+
+ + 48 + +
+
+
+
+ + 48 + +
+
+
+ + + + + + + + +
+
+
+ + 1 + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + + + + + + +
+
+
+ + team + +
+
+
+
+ + team + +
+
+
+ + + + + + + + + + + +
+
+
+ + + id + + +
+
+
+
+ + id + +
+
+
+ + + + + + + + +
+
+
+ + + name + + +
+
+
+
+ + name + +
+
+
+ + + + + + + + +
+
+
+ + + headquarters + + +
+
+
+
+ + headquarters + +
+
+
+ + + + + + + + + + + +
+
+
+ + + 1 + + +
+
+
+
+ + 1 + +
+
+
+ + + + + + + + +
+
+
+ + Preventers + +
+
+
+
+ + Preventers + +
+
+
+ + + + + + + + +
+
+
+ + + Sharp Tower + + +
+
+
+
+ + Sharp Tower + +
+
+
+ + + + + + + + + + + +
+
+
+ + 2 + +
+
+
+
+ + 2 + +
+
+
+ + + + + + + + +
+
+
+ + Z-Force + +
+
+
+
+ + Z-Force + +
+
+
+ + + + + + + + +
+
+
+

+ + Sister Margaret's Bar + +

+
+
+
+
+ + Sister Margaret's Bar + +
+
+
+ + + + + + + + +
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/docs/img/tutorial/relationships/select/relationships2.svg b/docs/img/tutorial/relationships/select/relationships2.svg deleted file mode 100644 index ed293b2350..0000000000 --- a/docs/img/tutorial/relationships/select/relationships2.svg +++ /dev/null @@ -1,57 +0,0 @@ -
hero
hero
id
id
name
name
secret_name
secret_name
age
age
team_id
team_id
1
1
Deadpond
Deadpond
Dive Wilson
Dive Wilson
null
null
2
2
2
2
Spider-Boy
Spider-Boy
Pedro Parqueador
Pedro Parqueador
null
null
null
null
3
3
Rusty-Man
Rusty-Man
Tommy Sharp
Tommy Sharp
48
48
1
1
team
team
id
id
name
name
headquarters
headquarters
1
1
Preventers
Preventers
Sharp Tower
Sharp Tower
2
2
Z-Force
Z-Force

Sister Margaret's Bar

Sister Margaret's Bar
Viewer does not support full SVG 1.1
diff --git a/docs/tutorial/connect/create-connected-rows.md b/docs/tutorial/connect/create-connected-rows.md index b01d20eb2b..2f952bf706 100644 --- a/docs/tutorial/connect/create-connected-rows.md +++ b/docs/tutorial/connect/create-connected-rows.md @@ -35,7 +35,7 @@ And after we finish working with the data in this chapter, the `hero` table will Each row in the table `hero` will point to a row in the table `team`: -table relationships +table relationships /// info diff --git a/docs/tutorial/connect/create-connected-tables.md b/docs/tutorial/connect/create-connected-tables.md index 630dfcbcc9..2b1a4f8758 100644 --- a/docs/tutorial/connect/create-connected-tables.md +++ b/docs/tutorial/connect/create-connected-tables.md @@ -39,7 +39,7 @@ To connect them, we will add another column to the hero table to point to each t This way each row in the table `hero` can point to a row in the table `team`: -table relationships +table relationships ## One-to-Many and Many-to-One diff --git a/docs/tutorial/connect/read-connected-data.md b/docs/tutorial/connect/read-connected-data.md index 3209f3cd0a..3fd4607c92 100644 --- a/docs/tutorial/connect/read-connected-data.md +++ b/docs/tutorial/connect/read-connected-data.md @@ -293,7 +293,7 @@ And then you tell the database `ON` which condition it should join those two tab But by default, only the rows from both left and right that match the condition will be returned. -table relationships +table relationships In this example of tables above 👆, it would return all the heroes, because every hero has a `team_id`, so every hero can be joined with the `team` table: @@ -318,7 +318,7 @@ But in the database that we are working with in the code above, **Spider-Boy** d So there's no way to join the **Spider-Boy** row with some row in the `team` table: -table relationships +table relationships Running the same SQL we used above, the resulting table would not include **Spider-Boy** 😱: diff --git a/docs/tutorial/indexes.md b/docs/tutorial/indexes.md index 80314ef274..459ee8ce34 100644 --- a/docs/tutorial/indexes.md +++ b/docs/tutorial/indexes.md @@ -34,35 +34,35 @@ Imagine a **dictionary**, a book with definitions of words. 📔 ...not a Python Let's say that you want to **find a word**, for example the word "**database**". You take the dictionary, and open it somewhere, for example in the middle. Maybe you see some definitions of words that start with `m`, like `manual`, so you conclude that you are in the letter `m` in the dictionary. - + You know that in the alphabet, the letter `d` for `database` comes **before** the letter `m` for `manual`. - + So, you know you have to search in the dictionary **before** the point you currently are. You still don't know where the word `database` is, because you don't know exactly where the letter `d` is in the dictionary, but you know that **it is not after** that point, you can now **discard the right half** of the dictionary in your search. - + Next, you **open the dictionary again**, but only taking into account the **half of the dictionary** that can contain the word you want, the **left part of the dictionary**. You open it in the middle of that left part and now you arrive maybe at the letter `f`. - + You know that `d` from `database` comes before `f`. So it has to be **before** that. But now you know that `database` **is not after** that point, and you can discard the dictionary from that point onward. - + Now you have a **small section of dictionary** to search (only a **quarter** of dictionary can have your word). You take that **quarter** of the pages at the start of the dictionary that can contain your word, and open it in the middle of that section. Maybe you arrive at the letter `c`. - + You know the word `database` has to be **after** that and **not before** that point, so you can discard the left part of that block of pages. - + You repeat this process **a few more times**, and you finally arrive at the letter `d`, you continue with the same process in that section for the letter `d` and you finally **find the word** `database`. 🎉 - + You had to open the dictionary a few times, maybe **5 or 10**. That's actually **very little work** compared to what it could have been. @@ -96,7 +96,7 @@ Open the index, and after **5 or 10 steps**, quickly find the topic "**database* Now you know that you need to find "**page 253**". But by looking at the closed book you still don't know where that page is, so you have to **find that page**. To find it, you can do the same process again, but this time, instead of searching for a **topic** in the **index**, you are searching for a **page number** in the **entire book**. And after **5 or 10 more steps**, you find the page 253 in Chapter 5. - + After this, even though this book is not a dictionary and has some particular content, you were able to **find the section** in the book that talks about a "**database**" in a **few steps** (say 10 or 20, instead of reading all the 500 pages). diff --git a/docs/tutorial/limit-and-offset.md b/docs/tutorial/limit-and-offset.md index 18e8aa14e2..215f57d1cd 100644 --- a/docs/tutorial/limit-and-offset.md +++ b/docs/tutorial/limit-and-offset.md @@ -6,7 +6,7 @@ And you also know how to get multiple rows while filtering them using `.where()` Now let's see how to get only a **range of results**. -table with first 3 rows selected +table with first 3 rows selected ## Create Data @@ -34,7 +34,7 @@ The special **select** object we get from `select()` also has a method `.limit() In this case, instead of getting all the 7 rows, we are limiting them to only get the first 3. -table with first 3 rows selected +table with first 3 rows selected ## Run the Program on the Command Line @@ -87,7 +87,7 @@ And then you can interact with the user interface to get the next page, and so o How do we get the next 3? -table with next rows selected, from 4 to 6 +table with next rows selected, from 4 to 6 We can use `.offset()`: @@ -134,7 +134,7 @@ Then to get the next batch of 3 rows we would offset all the ones we already saw The database right now has **only 7 rows**, so this query can only get 1 row. -table with the last row (7th) selected +table with the last row (7th) selected But don't worry, the database won't throw an error trying to get 3 rows when there's only one (as would happen with a Python list). diff --git a/docs/tutorial/many-to-many/create-data.md b/docs/tutorial/many-to-many/create-data.md index d53a64c5b2..3c8c0cfa6a 100644 --- a/docs/tutorial/many-to-many/create-data.md +++ b/docs/tutorial/many-to-many/create-data.md @@ -4,7 +4,7 @@ Let's continue from where we left and create some data. We'll create data for this same **many-to-many** relationship with a link table: -many-to-many table relationships +many-to-many table relationships We'll continue from where we left off with the previous code. diff --git a/docs/tutorial/many-to-many/create-models-with-link.md b/docs/tutorial/many-to-many/create-models-with-link.md index 587fa436a8..af6563e1ab 100644 --- a/docs/tutorial/many-to-many/create-models-with-link.md +++ b/docs/tutorial/many-to-many/create-models-with-link.md @@ -2,7 +2,7 @@ We'll now support **many-to-many** relationships using a **link table** like this: -many-to-many table relationships +many-to-many table relationships Let's start by defining the class models, including the **link table** model. diff --git a/docs/tutorial/many-to-many/index.md b/docs/tutorial/many-to-many/index.md index ebbefbe1ee..5cb3067e4a 100644 --- a/docs/tutorial/many-to-many/index.md +++ b/docs/tutorial/many-to-many/index.md @@ -57,7 +57,7 @@ We have a column in the `hero` table for the `team_id` that points to the ID of This is how we connect each `hero` with a `team`: -table relationships +table relationships Notice that each hero can only have **one** connection. But each team can receive **many** connections. In particular, the team **Preventers** has two heroes. @@ -83,7 +83,7 @@ As this will represent the **hero-team-link**, let's call the table `heroteamlin It would look like this: -many-to-many table relationships +many-to-many table relationships Notice that now the table `hero` **doesn't have a `team_id`** column anymore, it is replaced by this link table. diff --git a/docs/tutorial/relationship-attributes/back-populates.md b/docs/tutorial/relationship-attributes/back-populates.md index 7cb9de6c4a..9bdcbf0c28 100644 --- a/docs/tutorial/relationship-attributes/back-populates.md +++ b/docs/tutorial/relationship-attributes/back-populates.md @@ -10,7 +10,7 @@ So, what is that `back_populates` argument in each `Relationship()`? The value is a string with the name of the attribute in the **other** model class. - + That tells **SQLModel** that if something changes in this model, it should change that attribute in the other model, and it will work even before committing with the session (that would force a refresh of the data). @@ -175,7 +175,7 @@ It's quite simple code, it's just a string, but it might be confusing to think e The string in `back_populates` is the name of the attribute *in the other* model, that will reference *the current* model. - + So, in the class `Team`, we have an attribute `heroes` and we declare it with `Relationship(back_populates="team")`. @@ -210,6 +210,6 @@ So, if you are in the class `Hero`, the value of `back_populates` for any relati So, `back_populates` would most probably be something like `"hero"` or `"heroes"`. - + {* ./docs_src/tutorial/relationship_attributes/back_populates/tutorial003_py310.py ln[27:39] hl[27,34,37,39] *} From 725682059b7e8a8e739dbe7689bf76844b470853 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 11 May 2025 17:13:55 +0000 Subject: [PATCH 649/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index e6ed35417a..cd369e5220 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Docs +* 🍱 Update SVG files, a single file per diagram, sans-serif fonts. PR [#1373](https://github.com/fastapi/sqlmodel/pull/1373) by [@tiangolo](https://github.com/tiangolo). * 📝 Grammar tweak in `docs/tutorial/insert.md`. PR [#1368](https://github.com/fastapi/sqlmodel/pull/1368) by [@brettcannon](https://github.com/brettcannon). * 📝 Update `docs/tutorial/fastapi/relationships.md`. PR [#1365](https://github.com/fastapi/sqlmodel/pull/1365) by [@Foxerine](https://github.com/Foxerine). * ✏️ Tweak the grammar in `docs/learn/index.md`. PR [#1363](https://github.com/fastapi/sqlmodel/pull/1363) by [@brettcannon](https://github.com/brettcannon). From 17dde8157879a53bc46da29f8208465ffa8d752e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 22 May 2025 11:27:34 +0200 Subject: [PATCH 650/906] =?UTF-8?q?=F0=9F=94=A7=20Remove=20Google=20Analyt?= =?UTF-8?q?ics=20(#1386)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mkdocs.yml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 881e5e44fb..c59ccd245a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -187,22 +187,6 @@ markdown_extensions: markdown_include_variants: extra: - analytics: - provider: google - property: G-J8HVTT936W - feedback: - title: Was this page helpful? - ratings: - - icon: material/emoticon-happy-outline - name: This page was helpful - data: 1 - note: >- - Thanks for your feedback! - - icon: material/emoticon-sad-outline - name: This page could be improved - data: 0 - note: >- - Thanks for your feedback! social: - icon: fontawesome/brands/github-alt link: https://github.com/fastapi/sqlmodel From 1f08c0d3e01f74fb0c42aaf648205184b26db88d Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 22 May 2025 09:28:09 +0000 Subject: [PATCH 651/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index cd369e5220..a5a9d0c51f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* 🔧 Remove Google Analytics. PR [#1386](https://github.com/fastapi/sqlmodel/pull/1386) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump mkdocs-macros-plugin from 1.0.5 to 1.3.7. PR [#1354](https://github.com/fastapi/sqlmodel/pull/1354) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump griffe-typingdoc from 0.2.5 to 0.2.8. PR [#1359](https://github.com/fastapi/sqlmodel/pull/1359) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Update pre-commit requirement from <4.0.0,>=2.17.0 to >=2.17.0,<5.0.0. PR [#1360](https://github.com/fastapi/sqlmodel/pull/1360) by [@dependabot[bot]](https://github.com/apps/dependabot). From be69e8259752ef9269b5b9c2f81c8d5e702fead9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Jun 2025 23:25:30 +0200 Subject: [PATCH 652/906] =?UTF-8?q?=E2=AC=86=20Bump=20ruff=20from=200.11.7?= =?UTF-8?q?=20to=200.11.13=20(#1397)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.7 to 0.11.13. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.11.7...0.11.13) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.11.13 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 864051d30e..4e1cfdd4b4 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -3,7 +3,7 @@ pytest >=7.0.1,<9.0.0 coverage[toml] >=6.2,<8.0 mypy ==1.4.1 -ruff ==0.11.7 +ruff ==0.11.13 # For FastAPI tests fastapi >=0.103.2 httpx ==0.28.1 From 1777eee2992e7dd0450fef01a8b1ccf8258b5c5d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Jun 2025 23:25:39 +0200 Subject: [PATCH 653/906] =?UTF-8?q?=E2=AC=86=20Bump=20typer=20from=200.15.?= =?UTF-8?q?3=20to=200.16.0=20(#1393)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [typer](https://github.com/fastapi/typer) from 0.15.3 to 0.16.0. - [Release notes](https://github.com/fastapi/typer/releases) - [Changelog](https://github.com/fastapi/typer/blob/master/docs/release-notes.md) - [Commits](https://github.com/fastapi/typer/compare/0.15.3...0.16.0) --- updated-dependencies: - dependency-name: typer dependency-version: 0.16.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index ca92568732..d7418e2dae 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -13,6 +13,6 @@ cairosvg==2.7.1 # mkdocstrings[python]==0.25.1 griffe-typingdoc==0.2.8 # For griffe, it formats with black -typer == 0.15.3 +typer == 0.16.0 mkdocs-macros-plugin==1.3.7 markdown-include-variants==0.0.4 From b558f5523c503fba80ce5e75b6223b26322cb04c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Jun 2025 23:25:48 +0200 Subject: [PATCH 654/906] =?UTF-8?q?=E2=AC=86=20Bump=20cairosvg=20from=202.?= =?UTF-8?q?7.1=20to=202.8.2=20(#1383)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [cairosvg](https://github.com/Kozea/CairoSVG) from 2.7.1 to 2.8.2. - [Release notes](https://github.com/Kozea/CairoSVG/releases) - [Changelog](https://github.com/Kozea/CairoSVG/blob/main/NEWS.rst) - [Commits](https://github.com/Kozea/CairoSVG/compare/2.7.1...2.8.2) --- updated-dependencies: - dependency-name: cairosvg dependency-version: 2.8.2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index d7418e2dae..1377615a2f 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -9,7 +9,7 @@ pyyaml >=5.3.1,<7.0.0 # For image processing by Material for MkDocs pillow==11.2.1 # For image processing by Material for MkDocs -cairosvg==2.7.1 +cairosvg==2.8.2 # mkdocstrings[python]==0.25.1 griffe-typingdoc==0.2.8 # For griffe, it formats with black From b76b581d7a54dbf7b1058d13d0ab4788a2eb3c32 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Jun 2025 23:26:01 +0200 Subject: [PATCH 655/906] =?UTF-8?q?=E2=AC=86=20Bump=20mkdocs-material=20fr?= =?UTF-8?q?om=209.5.18=20to=209.6.14=20(#1378)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.5.18 to 9.6.14. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.5.18...9.6.14) --- updated-dependencies: - dependency-name: mkdocs-material dependency-version: 9.6.14 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 1377615a2f..849d2f89ec 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,6 +1,6 @@ -e . -r requirements-docs-tests.txt -mkdocs-material==9.5.18 +mkdocs-material==9.6.14 mdx-include >=1.4.1,<2.0.0 mkdocs-redirects>=1.2.1,<1.3.0 pyyaml >=5.3.1,<7.0.0 From 10aa48add25920a099d664f716665c03696aa70e Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 11 Jun 2025 21:26:11 +0000 Subject: [PATCH 656/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index a5a9d0c51f..80eaa75c91 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* ⬆ Bump ruff from 0.11.7 to 0.11.13. PR [#1397](https://github.com/fastapi/sqlmodel/pull/1397) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔧 Remove Google Analytics. PR [#1386](https://github.com/fastapi/sqlmodel/pull/1386) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump mkdocs-macros-plugin from 1.0.5 to 1.3.7. PR [#1354](https://github.com/fastapi/sqlmodel/pull/1354) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump griffe-typingdoc from 0.2.5 to 0.2.8. PR [#1359](https://github.com/fastapi/sqlmodel/pull/1359) by [@dependabot[bot]](https://github.com/apps/dependabot). From 202e40c6bbb34a44235a3772bdabb336c2490478 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 11 Jun 2025 21:26:21 +0000 Subject: [PATCH 657/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 80eaa75c91..516c654add 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* ⬆ Bump cairosvg from 2.7.1 to 2.8.2. PR [#1383](https://github.com/fastapi/sqlmodel/pull/1383) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.11.7 to 0.11.13. PR [#1397](https://github.com/fastapi/sqlmodel/pull/1397) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔧 Remove Google Analytics. PR [#1386](https://github.com/fastapi/sqlmodel/pull/1386) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump mkdocs-macros-plugin from 1.0.5 to 1.3.7. PR [#1354](https://github.com/fastapi/sqlmodel/pull/1354) by [@dependabot[bot]](https://github.com/apps/dependabot). From 63c018a6d2da395c67234a20f88607130dd93449 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 11 Jun 2025 21:26:22 +0000 Subject: [PATCH 658/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 516c654add..266da1587d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* ⬆ Bump typer from 0.15.3 to 0.16.0. PR [#1393](https://github.com/fastapi/sqlmodel/pull/1393) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump cairosvg from 2.7.1 to 2.8.2. PR [#1383](https://github.com/fastapi/sqlmodel/pull/1383) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.11.7 to 0.11.13. PR [#1397](https://github.com/fastapi/sqlmodel/pull/1397) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔧 Remove Google Analytics. PR [#1386](https://github.com/fastapi/sqlmodel/pull/1386) by [@tiangolo](https://github.com/tiangolo). From d42c85f905dc3965d1b3c34275b7c08fcd933532 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 11 Jun 2025 21:26:35 +0000 Subject: [PATCH 659/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 266da1587d..eccbc0bd8d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* ⬆ Bump mkdocs-material from 9.5.18 to 9.6.14. PR [#1378](https://github.com/fastapi/sqlmodel/pull/1378) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump typer from 0.15.3 to 0.16.0. PR [#1393](https://github.com/fastapi/sqlmodel/pull/1393) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump cairosvg from 2.7.1 to 2.8.2. PR [#1383](https://github.com/fastapi/sqlmodel/pull/1383) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.11.7 to 0.11.13. PR [#1397](https://github.com/fastapi/sqlmodel/pull/1397) by [@dependabot[bot]](https://github.com/apps/dependabot). From 75a79a3fcd2b5e2e0b4d24ac33dc5ae13ca70373 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 19 Jun 2025 15:23:26 +0200 Subject: [PATCH 660/906] =?UTF-8?q?=E2=9C=85=20Refactor=20tests=20to=20use?= =?UTF-8?q?=20autouse=20`clear=5Fsqlmodel`=20(#1406)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 4 ++-- tests/test_advanced/test_decimal/test_tutorial001.py | 2 +- tests/test_advanced/test_decimal/test_tutorial001_py310.py | 2 +- tests/test_advanced/test_uuid/test_tutorial001.py | 2 +- tests/test_advanced/test_uuid/test_tutorial001_py310.py | 2 +- tests/test_advanced/test_uuid/test_tutorial002.py | 2 +- tests/test_advanced/test_uuid/test_tutorial002_py310.py | 2 +- .../test_tutorial001_tutorial002.py | 4 ++-- tests/test_tutorial/test_code_structure/test_tutorial001.py | 2 +- .../test_code_structure/test_tutorial001_py310.py | 2 +- .../test_code_structure/test_tutorial001_py39.py | 2 +- tests/test_tutorial/test_code_structure/test_tutorial002.py | 2 +- .../test_code_structure/test_tutorial002_py310.py | 2 +- .../test_code_structure/test_tutorial002_py39.py | 2 +- .../test_create_connected_tables/test_tutorial001.py | 2 +- .../test_create_connected_tables/test_tutorial001_py310.py | 2 +- .../test_connect/test_delete/test_tutorial001.py | 2 +- .../test_connect/test_delete/test_tutorial001_py310.py | 2 +- .../test_connect/test_insert/test_tutorial001.py | 2 +- .../test_connect/test_insert/test_tutorial001_py310.py | 2 +- .../test_select/test_tutorial001_py310_tutorial002_py310.py | 4 ++-- .../test_connect/test_select/test_tutorial001_tutorial002.py | 4 ++-- .../test_connect/test_select/test_tutorial003.py | 2 +- .../test_connect/test_select/test_tutorial003_py310.py | 2 +- .../test_connect/test_select/test_tutorial004.py | 2 +- .../test_connect/test_select/test_tutorial004_py310.py | 2 +- .../test_connect/test_select/test_tutorial005.py | 2 +- .../test_connect/test_select/test_tutorial005_py310.py | 2 +- 28 files changed, 32 insertions(+), 32 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index a95eb3279f..9e8a45cc2c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,8 +14,8 @@ docs_src_path = top_level_path / "docs_src" -@pytest.fixture() -def clear_sqlmodel(): +@pytest.fixture(autouse=True) +def clear_sqlmodel() -> Any: # Clear the tables in the metadata for the default base model SQLModel.metadata.clear() # Clear the Models associated with the registry, to avoid warnings diff --git a/tests/test_advanced/test_decimal/test_tutorial001.py b/tests/test_advanced/test_decimal/test_tutorial001.py index 1dafdfb322..2dc562209f 100644 --- a/tests/test_advanced/test_decimal/test_tutorial001.py +++ b/tests/test_advanced/test_decimal/test_tutorial001.py @@ -30,7 +30,7 @@ ] -def test_tutorial(clear_sqlmodel): +def test_tutorial(): from docs_src.advanced.decimal import tutorial001 as mod mod.sqlite_url = "sqlite://" diff --git a/tests/test_advanced/test_decimal/test_tutorial001_py310.py b/tests/test_advanced/test_decimal/test_tutorial001_py310.py index f58ea11a7c..4cda8b4653 100644 --- a/tests/test_advanced/test_decimal/test_tutorial001_py310.py +++ b/tests/test_advanced/test_decimal/test_tutorial001_py310.py @@ -31,7 +31,7 @@ @needs_py310 -def test_tutorial(clear_sqlmodel): +def test_tutorial(): from docs_src.advanced.decimal import tutorial001_py310 as mod mod.sqlite_url = "sqlite://" diff --git a/tests/test_advanced/test_uuid/test_tutorial001.py b/tests/test_advanced/test_uuid/test_tutorial001.py index 405195f8e9..b9d5a36800 100644 --- a/tests/test_advanced/test_uuid/test_tutorial001.py +++ b/tests/test_advanced/test_uuid/test_tutorial001.py @@ -6,7 +6,7 @@ from ...conftest import get_testing_print_function -def test_tutorial(clear_sqlmodel) -> None: +def test_tutorial() -> None: from docs_src.advanced.uuid import tutorial001 as mod mod.sqlite_url = "sqlite://" diff --git a/tests/test_advanced/test_uuid/test_tutorial001_py310.py b/tests/test_advanced/test_uuid/test_tutorial001_py310.py index ee8cb085df..1250c32872 100644 --- a/tests/test_advanced/test_uuid/test_tutorial001_py310.py +++ b/tests/test_advanced/test_uuid/test_tutorial001_py310.py @@ -7,7 +7,7 @@ @needs_py310 -def test_tutorial(clear_sqlmodel) -> None: +def test_tutorial() -> None: from docs_src.advanced.uuid import tutorial001_py310 as mod mod.sqlite_url = "sqlite://" diff --git a/tests/test_advanced/test_uuid/test_tutorial002.py b/tests/test_advanced/test_uuid/test_tutorial002.py index cefd95ba49..c9f4e5a35d 100644 --- a/tests/test_advanced/test_uuid/test_tutorial002.py +++ b/tests/test_advanced/test_uuid/test_tutorial002.py @@ -6,7 +6,7 @@ from ...conftest import get_testing_print_function -def test_tutorial(clear_sqlmodel) -> None: +def test_tutorial() -> None: from docs_src.advanced.uuid import tutorial002 as mod mod.sqlite_url = "sqlite://" diff --git a/tests/test_advanced/test_uuid/test_tutorial002_py310.py b/tests/test_advanced/test_uuid/test_tutorial002_py310.py index 96f85c5333..ba472e30fd 100644 --- a/tests/test_advanced/test_uuid/test_tutorial002_py310.py +++ b/tests/test_advanced/test_uuid/test_tutorial002_py310.py @@ -7,7 +7,7 @@ @needs_py310 -def test_tutorial(clear_sqlmodel) -> None: +def test_tutorial() -> None: from docs_src.advanced.uuid import tutorial002_py310 as mod mod.sqlite_url = "sqlite://" diff --git a/tests/test_tutorial/test_automatic_id_none_refresh/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_automatic_id_none_refresh/test_tutorial001_tutorial002.py index 399f431b49..5c2504710b 100644 --- a/tests/test_tutorial/test_automatic_id_none_refresh/test_tutorial001_tutorial002.py +++ b/tests/test_tutorial/test_automatic_id_none_refresh/test_tutorial001_tutorial002.py @@ -133,7 +133,7 @@ def check_calls(calls: List[List[Union[str, Dict[str, Any]]]]): ] -def test_tutorial_001(clear_sqlmodel): +def test_tutorial_001(): from docs_src.tutorial.automatic_id_none_refresh import tutorial001 as mod mod.sqlite_url = "sqlite://" @@ -147,7 +147,7 @@ def test_tutorial_001(clear_sqlmodel): check_calls(calls) -def test_tutorial_002(clear_sqlmodel): +def test_tutorial_002(): from docs_src.tutorial.automatic_id_none_refresh import tutorial002 as mod mod.sqlite_url = "sqlite://" diff --git a/tests/test_tutorial/test_code_structure/test_tutorial001.py b/tests/test_tutorial/test_code_structure/test_tutorial001.py index c6e3158360..109c1ef5c6 100644 --- a/tests/test_tutorial/test_code_structure/test_tutorial001.py +++ b/tests/test_tutorial/test_code_structure/test_tutorial001.py @@ -22,7 +22,7 @@ ] -def test_tutorial(clear_sqlmodel): +def test_tutorial(): from docs_src.tutorial.code_structure.tutorial001 import app, database database.sqlite_url = "sqlite://" diff --git a/tests/test_tutorial/test_code_structure/test_tutorial001_py310.py b/tests/test_tutorial/test_code_structure/test_tutorial001_py310.py index 44d9d920fa..126bef25e1 100644 --- a/tests/test_tutorial/test_code_structure/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_code_structure/test_tutorial001_py310.py @@ -23,7 +23,7 @@ @needs_py310 -def test_tutorial(clear_sqlmodel): +def test_tutorial(): from docs_src.tutorial.code_structure.tutorial001_py310 import app, database database.sqlite_url = "sqlite://" diff --git a/tests/test_tutorial/test_code_structure/test_tutorial001_py39.py b/tests/test_tutorial/test_code_structure/test_tutorial001_py39.py index b17917cff2..02f692eac8 100644 --- a/tests/test_tutorial/test_code_structure/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_code_structure/test_tutorial001_py39.py @@ -23,7 +23,7 @@ @needs_py39 -def test_tutorial(clear_sqlmodel): +def test_tutorial(): from docs_src.tutorial.code_structure.tutorial001_py39 import app, database database.sqlite_url = "sqlite://" diff --git a/tests/test_tutorial/test_code_structure/test_tutorial002.py b/tests/test_tutorial/test_code_structure/test_tutorial002.py index 8e7ac8f173..ccbb849097 100644 --- a/tests/test_tutorial/test_code_structure/test_tutorial002.py +++ b/tests/test_tutorial/test_code_structure/test_tutorial002.py @@ -22,7 +22,7 @@ ] -def test_tutorial(clear_sqlmodel): +def test_tutorial(): from docs_src.tutorial.code_structure.tutorial002 import app, database database.sqlite_url = "sqlite://" diff --git a/tests/test_tutorial/test_code_structure/test_tutorial002_py310.py b/tests/test_tutorial/test_code_structure/test_tutorial002_py310.py index 3eafdee831..be28486652 100644 --- a/tests/test_tutorial/test_code_structure/test_tutorial002_py310.py +++ b/tests/test_tutorial/test_code_structure/test_tutorial002_py310.py @@ -23,7 +23,7 @@ @needs_py310 -def test_tutorial(clear_sqlmodel): +def test_tutorial(): from docs_src.tutorial.code_structure.tutorial002_py310 import app, database database.sqlite_url = "sqlite://" diff --git a/tests/test_tutorial/test_code_structure/test_tutorial002_py39.py b/tests/test_tutorial/test_code_structure/test_tutorial002_py39.py index 9b5eb670c2..55f6a435dc 100644 --- a/tests/test_tutorial/test_code_structure/test_tutorial002_py39.py +++ b/tests/test_tutorial/test_code_structure/test_tutorial002_py39.py @@ -23,7 +23,7 @@ @needs_py39 -def test_tutorial(clear_sqlmodel): +def test_tutorial(): from docs_src.tutorial.code_structure.tutorial002_py39 import app, database database.sqlite_url = "sqlite://" diff --git a/tests/test_tutorial/test_connect/test_create_connected_tables/test_tutorial001.py b/tests/test_tutorial/test_connect/test_create_connected_tables/test_tutorial001.py index e3e0799246..265a05931c 100644 --- a/tests/test_tutorial/test_connect/test_create_connected_tables/test_tutorial001.py +++ b/tests/test_tutorial/test_connect/test_create_connected_tables/test_tutorial001.py @@ -3,7 +3,7 @@ from sqlmodel import create_engine -def test_tutorial001(clear_sqlmodel): +def test_tutorial001(): from docs_src.tutorial.connect.create_tables import tutorial001 as mod mod.sqlite_url = "sqlite://" diff --git a/tests/test_tutorial/test_connect/test_create_connected_tables/test_tutorial001_py310.py b/tests/test_tutorial/test_connect/test_create_connected_tables/test_tutorial001_py310.py index ec2990ebfb..95f15a4266 100644 --- a/tests/test_tutorial/test_connect/test_create_connected_tables/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_connect/test_create_connected_tables/test_tutorial001_py310.py @@ -6,7 +6,7 @@ @needs_py310 -def test_tutorial001(clear_sqlmodel): +def test_tutorial001(): from docs_src.tutorial.connect.create_tables import tutorial001_py310 as mod mod.sqlite_url = "sqlite://" diff --git a/tests/test_tutorial/test_connect/test_delete/test_tutorial001.py b/tests/test_tutorial/test_connect/test_delete/test_tutorial001.py index a5db3867e4..1a9fe293ba 100644 --- a/tests/test_tutorial/test_connect/test_delete/test_tutorial001.py +++ b/tests/test_tutorial/test_connect/test_delete/test_tutorial001.py @@ -58,7 +58,7 @@ ] -def test_tutorial(clear_sqlmodel): +def test_tutorial(): from docs_src.tutorial.connect.delete import tutorial001 as mod mod.sqlite_url = "sqlite://" diff --git a/tests/test_tutorial/test_connect/test_delete/test_tutorial001_py310.py b/tests/test_tutorial/test_connect/test_delete/test_tutorial001_py310.py index edc70b8a3d..f1bef3ed02 100644 --- a/tests/test_tutorial/test_connect/test_delete/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_connect/test_delete/test_tutorial001_py310.py @@ -59,7 +59,7 @@ @needs_py310 -def test_tutorial(clear_sqlmodel): +def test_tutorial(): from docs_src.tutorial.connect.delete import tutorial001_py310 as mod mod.sqlite_url = "sqlite://" diff --git a/tests/test_tutorial/test_connect/test_insert/test_tutorial001.py b/tests/test_tutorial/test_connect/test_insert/test_tutorial001.py index 8c8a303a21..cfc08ee854 100644 --- a/tests/test_tutorial/test_connect/test_insert/test_tutorial001.py +++ b/tests/test_tutorial/test_connect/test_insert/test_tutorial001.py @@ -38,7 +38,7 @@ ] -def test_tutorial001(clear_sqlmodel): +def test_tutorial001(): from docs_src.tutorial.connect.insert import tutorial001 as mod mod.sqlite_url = "sqlite://" diff --git a/tests/test_tutorial/test_connect/test_insert/test_tutorial001_py310.py b/tests/test_tutorial/test_connect/test_insert/test_tutorial001_py310.py index 854c0068ab..6dabc10b80 100644 --- a/tests/test_tutorial/test_connect/test_insert/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_connect/test_insert/test_tutorial001_py310.py @@ -39,7 +39,7 @@ @needs_py310 -def test_tutorial001(clear_sqlmodel): +def test_tutorial001(): from docs_src.tutorial.connect.insert import tutorial001_py310 as mod mod.sqlite_url = "sqlite://" diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial001_py310_tutorial002_py310.py b/tests/test_tutorial/test_connect/test_select/test_tutorial001_py310_tutorial002_py310.py index d3bab7f669..4809d79b68 100644 --- a/tests/test_tutorial/test_connect/test_select/test_tutorial001_py310_tutorial002_py310.py +++ b/tests/test_tutorial/test_connect/test_select/test_tutorial001_py310_tutorial002_py310.py @@ -63,7 +63,7 @@ @needs_py310 -def test_tutorial001(clear_sqlmodel): +def test_tutorial001(): from docs_src.tutorial.connect.select import tutorial001_py310 as mod mod.sqlite_url = "sqlite://" @@ -78,7 +78,7 @@ def test_tutorial001(clear_sqlmodel): @needs_py310 -def test_tutorial002(clear_sqlmodel): +def test_tutorial002(): from docs_src.tutorial.connect.select import tutorial002_py310 as mod mod.sqlite_url = "sqlite://" diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_connect/test_select/test_tutorial001_tutorial002.py index 541a8ee00f..c0d6b59dd9 100644 --- a/tests/test_tutorial/test_connect/test_select/test_tutorial001_tutorial002.py +++ b/tests/test_tutorial/test_connect/test_select/test_tutorial001_tutorial002.py @@ -62,7 +62,7 @@ ] -def test_tutorial001(clear_sqlmodel): +def test_tutorial001(): from docs_src.tutorial.connect.select import tutorial001 as mod mod.sqlite_url = "sqlite://" @@ -76,7 +76,7 @@ def test_tutorial001(clear_sqlmodel): assert calls == expected_calls -def test_tutorial002(clear_sqlmodel): +def test_tutorial002(): from docs_src.tutorial.connect.select import tutorial002 as mod mod.sqlite_url = "sqlite://" diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial003.py b/tests/test_tutorial/test_connect/test_select/test_tutorial003.py index 2eab135add..f309e1c44e 100644 --- a/tests/test_tutorial/test_connect/test_select/test_tutorial003.py +++ b/tests/test_tutorial/test_connect/test_select/test_tutorial003.py @@ -74,7 +74,7 @@ ] -def test_tutorial(clear_sqlmodel): +def test_tutorial(): from docs_src.tutorial.connect.select import tutorial003 as mod mod.sqlite_url = "sqlite://" diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial003_py310.py b/tests/test_tutorial/test_connect/test_select/test_tutorial003_py310.py index 5b710c4358..e826ce44ae 100644 --- a/tests/test_tutorial/test_connect/test_select/test_tutorial003_py310.py +++ b/tests/test_tutorial/test_connect/test_select/test_tutorial003_py310.py @@ -75,7 +75,7 @@ @needs_py310 -def test_tutorial(clear_sqlmodel): +def test_tutorial(): from docs_src.tutorial.connect.select import tutorial003_py310 as mod mod.sqlite_url = "sqlite://" diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial004.py b/tests/test_tutorial/test_connect/test_select/test_tutorial004.py index ebc273feb1..a33c814856 100644 --- a/tests/test_tutorial/test_connect/test_select/test_tutorial004.py +++ b/tests/test_tutorial/test_connect/test_select/test_tutorial004.py @@ -48,7 +48,7 @@ ] -def test_tutorial(clear_sqlmodel): +def test_tutorial(): from docs_src.tutorial.connect.select import tutorial004 as mod mod.sqlite_url = "sqlite://" diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial004_py310.py b/tests/test_tutorial/test_connect/test_select/test_tutorial004_py310.py index 72974ec6cf..33dd8a4329 100644 --- a/tests/test_tutorial/test_connect/test_select/test_tutorial004_py310.py +++ b/tests/test_tutorial/test_connect/test_select/test_tutorial004_py310.py @@ -49,7 +49,7 @@ @needs_py310 -def test_tutorial(clear_sqlmodel): +def test_tutorial(): from docs_src.tutorial.connect.select import tutorial004_py310 as mod mod.sqlite_url = "sqlite://" diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial005.py b/tests/test_tutorial/test_connect/test_select/test_tutorial005.py index 400c6483cb..f7ad78dc65 100644 --- a/tests/test_tutorial/test_connect/test_select/test_tutorial005.py +++ b/tests/test_tutorial/test_connect/test_select/test_tutorial005.py @@ -50,7 +50,7 @@ ] -def test_tutorial(clear_sqlmodel): +def test_tutorial(): from docs_src.tutorial.connect.select import tutorial005 as mod mod.sqlite_url = "sqlite://" diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial005_py310.py b/tests/test_tutorial/test_connect/test_select/test_tutorial005_py310.py index a7332c18a7..8cddb6455a 100644 --- a/tests/test_tutorial/test_connect/test_select/test_tutorial005_py310.py +++ b/tests/test_tutorial/test_connect/test_select/test_tutorial005_py310.py @@ -51,7 +51,7 @@ @needs_py310 -def test_tutorial(clear_sqlmodel): +def test_tutorial(): from docs_src.tutorial.connect.select import tutorial005_py310 as mod mod.sqlite_url = "sqlite://" From af52d6573f0862072658dfa5662573bf39bd9c56 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 19 Jun 2025 13:23:47 +0000 Subject: [PATCH 661/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index eccbc0bd8d..37e3dfd744 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* ✅ Refactor tests to use autouse `clear_sqlmodel`. PR [#1406](https://github.com/fastapi/sqlmodel/pull/1406) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump mkdocs-material from 9.5.18 to 9.6.14. PR [#1378](https://github.com/fastapi/sqlmodel/pull/1378) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump typer from 0.15.3 to 0.16.0. PR [#1393](https://github.com/fastapi/sqlmodel/pull/1393) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump cairosvg from 2.7.1 to 2.8.2. PR [#1383](https://github.com/fastapi/sqlmodel/pull/1383) by [@dependabot[bot]](https://github.com/apps/dependabot). From f1c9d15525b4e9da0644e95991ce145c34f8b86e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 19 Jun 2025 16:29:32 +0200 Subject: [PATCH 662/906] =?UTF-8?q?=E2=9C=85=20Simplify=20tests=20setup,?= =?UTF-8?q?=20one=20test=20file=20for=20multiple=20source=20variants=20(#1?= =?UTF-8?q?407)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 23 ++- ...est_tutorial001_py310_tutorial002_py310.py | 163 ------------------ .../test_tutorial001_tutorial002.py | 50 +++--- 3 files changed, 43 insertions(+), 193 deletions(-) delete mode 100644 tests/test_tutorial/test_automatic_id_none_refresh/test_tutorial001_py310_tutorial002_py310.py diff --git a/tests/conftest.py b/tests/conftest.py index 9e8a45cc2c..98a4d2b7e6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,10 @@ import shutil import subprocess import sys +from dataclasses import dataclass, field from pathlib import Path -from typing import Any, Callable, Dict, List, Union +from typing import Any, Callable, Dict, Generator, List, Union +from unittest.mock import patch import pytest from pydantic import BaseModel @@ -26,7 +28,7 @@ def clear_sqlmodel() -> Any: @pytest.fixture() -def cov_tmp_path(tmp_path: Path): +def cov_tmp_path(tmp_path: Path) -> Generator[Path, None, None]: yield tmp_path for coverage_path in tmp_path.glob(".coverage*"): coverage_destiny_path = top_level_path / coverage_path.name @@ -53,8 +55,8 @@ def coverage_run(*, module: str, cwd: Union[str, Path]) -> subprocess.CompletedP def get_testing_print_function( calls: List[List[Union[str, Dict[str, Any]]]], ) -> Callable[..., Any]: - def new_print(*args): - data = [] + def new_print(*args: Any) -> None: + data: List[Any] = [] for arg in args: if isinstance(arg, BaseModel): data.append(arg.model_dump()) @@ -71,6 +73,19 @@ def new_print(*args): return new_print +@dataclass +class PrintMock: + calls: List[Any] = field(default_factory=list) + + +@pytest.fixture(name="print_mock") +def print_mock_fixture() -> Generator[PrintMock, None, None]: + print_mock = PrintMock() + new_print = get_testing_print_function(print_mock.calls) + with patch("builtins.print", new=new_print): + yield print_mock + + needs_pydanticv2 = pytest.mark.skipif(not IS_PYDANTIC_V2, reason="requires Pydantic v2") needs_pydanticv1 = pytest.mark.skipif(IS_PYDANTIC_V2, reason="requires Pydantic v1") diff --git a/tests/test_tutorial/test_automatic_id_none_refresh/test_tutorial001_py310_tutorial002_py310.py b/tests/test_tutorial/test_automatic_id_none_refresh/test_tutorial001_py310_tutorial002_py310.py deleted file mode 100644 index 9ffcd8ae33..0000000000 --- a/tests/test_tutorial/test_automatic_id_none_refresh/test_tutorial001_py310_tutorial002_py310.py +++ /dev/null @@ -1,163 +0,0 @@ -from typing import Any, Dict, List, Union -from unittest.mock import patch - -from sqlmodel import create_engine - -from tests.conftest import get_testing_print_function, needs_py310 - - -def check_calls(calls: List[List[Union[str, Dict[str, Any]]]]): - assert calls[0] == ["Before interacting with the database"] - assert calls[1] == [ - "Hero 1:", - { - "id": None, - "name": "Deadpond", - "secret_name": "Dive Wilson", - "age": None, - }, - ] - assert calls[2] == [ - "Hero 2:", - { - "id": None, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "age": None, - }, - ] - assert calls[3] == [ - "Hero 3:", - { - "id": None, - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - }, - ] - assert calls[4] == ["After adding to the session"] - assert calls[5] == [ - "Hero 1:", - { - "id": None, - "name": "Deadpond", - "secret_name": "Dive Wilson", - "age": None, - }, - ] - assert calls[6] == [ - "Hero 2:", - { - "id": None, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "age": None, - }, - ] - assert calls[7] == [ - "Hero 3:", - { - "id": None, - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - }, - ] - assert calls[8] == ["After committing the session"] - assert calls[9] == ["Hero 1:", {}] - assert calls[10] == ["Hero 2:", {}] - assert calls[11] == ["Hero 3:", {}] - assert calls[12] == ["After committing the session, show IDs"] - assert calls[13] == ["Hero 1 ID:", 1] - assert calls[14] == ["Hero 2 ID:", 2] - assert calls[15] == ["Hero 3 ID:", 3] - assert calls[16] == ["After committing the session, show names"] - assert calls[17] == ["Hero 1 name:", "Deadpond"] - assert calls[18] == ["Hero 2 name:", "Spider-Boy"] - assert calls[19] == ["Hero 3 name:", "Rusty-Man"] - assert calls[20] == ["After refreshing the heroes"] - assert calls[21] == [ - "Hero 1:", - { - "id": 1, - "name": "Deadpond", - "secret_name": "Dive Wilson", - "age": None, - }, - ] - assert calls[22] == [ - "Hero 2:", - { - "id": 2, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "age": None, - }, - ] - assert calls[23] == [ - "Hero 3:", - { - "id": 3, - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - }, - ] - assert calls[24] == ["After the session closes"] - assert calls[21] == [ - "Hero 1:", - { - "id": 1, - "name": "Deadpond", - "secret_name": "Dive Wilson", - "age": None, - }, - ] - assert calls[22] == [ - "Hero 2:", - { - "id": 2, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "age": None, - }, - ] - assert calls[23] == [ - "Hero 3:", - { - "id": 3, - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - }, - ] - - -@needs_py310 -def test_tutorial_001(clear_sqlmodel): - from docs_src.tutorial.automatic_id_none_refresh import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - check_calls(calls) - - -@needs_py310 -def test_tutorial_002(clear_sqlmodel): - from docs_src.tutorial.automatic_id_none_refresh import tutorial002_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - check_calls(calls) diff --git a/tests/test_tutorial/test_automatic_id_none_refresh/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_automatic_id_none_refresh/test_tutorial001_tutorial002.py index 5c2504710b..7233e40be8 100644 --- a/tests/test_tutorial/test_automatic_id_none_refresh/test_tutorial001_tutorial002.py +++ b/tests/test_tutorial/test_automatic_id_none_refresh/test_tutorial001_tutorial002.py @@ -1,12 +1,14 @@ +import importlib +from types import ModuleType from typing import Any, Dict, List, Union -from unittest.mock import patch +import pytest from sqlmodel import create_engine -from tests.conftest import get_testing_print_function +from tests.conftest import PrintMock, needs_py310 -def check_calls(calls: List[List[Union[str, Dict[str, Any]]]]): +def check_calls(calls: List[List[Union[str, Dict[str, Any]]]]) -> None: assert calls[0] == ["Before interacting with the database"] assert calls[1] == [ "Hero 1:", @@ -133,29 +135,25 @@ def check_calls(calls: List[List[Union[str, Dict[str, Any]]]]): ] -def test_tutorial_001(): - from docs_src.tutorial.automatic_id_none_refresh import tutorial001 as mod +@pytest.fixture( + name="module", + params=[ + "tutorial001", + "tutorial002", + pytest.param("tutorial001_py310", marks=needs_py310), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module( + f"docs_src.tutorial.automatic_id_none_refresh.{request.param}" + ) + module.sqlite_url = "sqlite://" + module.engine = create_engine(module.sqlite_url) - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] + return module - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - check_calls(calls) - - -def test_tutorial_002(): - from docs_src.tutorial.automatic_id_none_refresh import tutorial002 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - check_calls(calls) +def test_tutorial_001_tutorial_002(print_mock: PrintMock, module: ModuleType) -> None: + module.main() + check_calls(print_mock.calls) From f480441397b762e3dcf155b2fa6b2274736b2ca1 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 19 Jun 2025 14:29:48 +0000 Subject: [PATCH 663/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 37e3dfd744..cedbccc4c7 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* ✅ Simplify tests setup, one test file for multiple source variants. PR [#1407](https://github.com/fastapi/sqlmodel/pull/1407) by [@tiangolo](https://github.com/tiangolo). * ✅ Refactor tests to use autouse `clear_sqlmodel`. PR [#1406](https://github.com/fastapi/sqlmodel/pull/1406) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump mkdocs-material from 9.5.18 to 9.6.14. PR [#1378](https://github.com/fastapi/sqlmodel/pull/1378) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump typer from 0.15.3 to 0.16.0. PR [#1393](https://github.com/fastapi/sqlmodel/pull/1393) by [@dependabot[bot]](https://github.com/apps/dependabot). From 8ae5f9b6c8ec7883f0e95277cc44340cf3489335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 19 Jun 2025 18:19:22 +0200 Subject: [PATCH 664/906] =?UTF-8?q?=E2=9C=85=20Simplify=20tests=20for=20`t?= =?UTF-8?q?ests/test=5Ftutorial/test=5Fcode=5Fstructure/test=5Ftutorial001?= =?UTF-8?q?.py`,=20one=20test=20file=20for=20multiple=20variants=20(#1408)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_code_structure/test_tutorial001.py | 47 ++++++++++++++----- .../test_tutorial001_py310.py | 38 --------------- .../test_tutorial001_py39.py | 38 --------------- 3 files changed, 34 insertions(+), 89 deletions(-) delete mode 100644 tests/test_tutorial/test_code_structure/test_tutorial001_py310.py delete mode 100644 tests/test_tutorial/test_code_structure/test_tutorial001_py39.py diff --git a/tests/test_tutorial/test_code_structure/test_tutorial001.py b/tests/test_tutorial/test_code_structure/test_tutorial001.py index 109c1ef5c6..99ae5c00f0 100644 --- a/tests/test_tutorial/test_code_structure/test_tutorial001.py +++ b/tests/test_tutorial/test_code_structure/test_tutorial001.py @@ -1,8 +1,11 @@ -from unittest.mock import patch +import importlib +from dataclasses import dataclass +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from tests.conftest import PrintMock, needs_py39, needs_py310 expected_calls = [ [ @@ -22,16 +25,34 @@ ] -def test_tutorial(): - from docs_src.tutorial.code_structure.tutorial001 import app, database +@dataclass +class Modules: + app: ModuleType + database: ModuleType - database.sqlite_url = "sqlite://" - database.engine = create_engine(database.sqlite_url) - app.engine = database.engine - calls = [] - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - app.main() - assert calls == expected_calls +@pytest.fixture( + name="modules", + params=[ + "tutorial001", + pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_modules(request: pytest.FixtureRequest) -> Modules: + app_module = importlib.import_module( + f"docs_src.tutorial.code_structure.{request.param}.app" + ) + database_module = importlib.import_module( + f"docs_src.tutorial.code_structure.{request.param}.database" + ) + database_module.sqlite_url = "sqlite://" + database_module.engine = create_engine(database_module.sqlite_url) + app_module.engine = database_module.engine + + return Modules(app=app_module, database=database_module) + + +def test_tutorial(print_mock: PrintMock, modules: Modules): + modules.app.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_code_structure/test_tutorial001_py310.py b/tests/test_tutorial/test_code_structure/test_tutorial001_py310.py deleted file mode 100644 index 126bef25e1..0000000000 --- a/tests/test_tutorial/test_code_structure/test_tutorial001_py310.py +++ /dev/null @@ -1,38 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Created hero:", - { - "id": 1, - "name": "Deadpond", - "age": None, - "secret_name": "Dive Wilson", - "team_id": 1, - }, - ], - [ - "Hero's team:", - {"name": "Z-Force", "headquarters": "Sister Margaret's Bar", "id": 1}, - ], -] - - -@needs_py310 -def test_tutorial(): - from docs_src.tutorial.code_structure.tutorial001_py310 import app, database - - database.sqlite_url = "sqlite://" - database.engine = create_engine(database.sqlite_url) - app.engine = database.engine - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - app.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_code_structure/test_tutorial001_py39.py b/tests/test_tutorial/test_code_structure/test_tutorial001_py39.py deleted file mode 100644 index 02f692eac8..0000000000 --- a/tests/test_tutorial/test_code_structure/test_tutorial001_py39.py +++ /dev/null @@ -1,38 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py39 - -expected_calls = [ - [ - "Created hero:", - { - "id": 1, - "name": "Deadpond", - "age": None, - "secret_name": "Dive Wilson", - "team_id": 1, - }, - ], - [ - "Hero's team:", - {"name": "Z-Force", "headquarters": "Sister Margaret's Bar", "id": 1}, - ], -] - - -@needs_py39 -def test_tutorial(): - from docs_src.tutorial.code_structure.tutorial001_py39 import app, database - - database.sqlite_url = "sqlite://" - database.engine = create_engine(database.sqlite_url) - app.engine = database.engine - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - app.main() - assert calls == expected_calls From 131559c0a751198c97f8784a97b0c73bd38fab3f Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 19 Jun 2025 16:19:42 +0000 Subject: [PATCH 665/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index cedbccc4c7..ca4c2191fe 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* ✅ Simplify tests for `tests/test_tutorial/test_code_structure/test_tutorial001.py`, one test file for multiple variants. PR [#1408](https://github.com/fastapi/sqlmodel/pull/1408) by [@tiangolo](https://github.com/tiangolo). * ✅ Simplify tests setup, one test file for multiple source variants. PR [#1407](https://github.com/fastapi/sqlmodel/pull/1407) by [@tiangolo](https://github.com/tiangolo). * ✅ Refactor tests to use autouse `clear_sqlmodel`. PR [#1406](https://github.com/fastapi/sqlmodel/pull/1406) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump mkdocs-material from 9.5.18 to 9.6.14. PR [#1378](https://github.com/fastapi/sqlmodel/pull/1378) by [@dependabot[bot]](https://github.com/apps/dependabot). From 60f7e44f2cd5fecd9cdabaf9f92355adc40d9594 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Jun 2025 14:19:27 +0200 Subject: [PATCH 666/906] =?UTF-8?q?=E2=AC=86=20Bump=20ruff=20from=200.11.1?= =?UTF-8?q?3=20to=200.12.0=20(#1403)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.13 to 0.12.0. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.11.13...0.12.0) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.12.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 4e1cfdd4b4..ee3af92244 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -3,7 +3,7 @@ pytest >=7.0.1,<9.0.0 coverage[toml] >=6.2,<8.0 mypy ==1.4.1 -ruff ==0.11.13 +ruff ==0.12.0 # For FastAPI tests fastapi >=0.103.2 httpx ==0.28.1 From 6d68f0dbba8b56b87a83e321dc515cf1e08b4983 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 24 Jun 2025 14:19:42 +0200 Subject: [PATCH 667/906] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#1374)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.11.8 → v0.12.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.8...v0.12.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2a688e2b42..946d24f359 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.8 + rev: v0.12.0 hooks: - id: ruff args: From eebc296397eaaa8a76bd4ea22546d5738cca07f9 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 24 Jun 2025 12:19:45 +0000 Subject: [PATCH 668/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index ca4c2191fe..0139c9b5df 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* ⬆ Bump ruff from 0.11.13 to 0.12.0. PR [#1403](https://github.com/fastapi/sqlmodel/pull/1403) by [@dependabot[bot]](https://github.com/apps/dependabot). * ✅ Simplify tests for `tests/test_tutorial/test_code_structure/test_tutorial001.py`, one test file for multiple variants. PR [#1408](https://github.com/fastapi/sqlmodel/pull/1408) by [@tiangolo](https://github.com/tiangolo). * ✅ Simplify tests setup, one test file for multiple source variants. PR [#1407](https://github.com/fastapi/sqlmodel/pull/1407) by [@tiangolo](https://github.com/tiangolo). * ✅ Refactor tests to use autouse `clear_sqlmodel`. PR [#1406](https://github.com/fastapi/sqlmodel/pull/1406) by [@tiangolo](https://github.com/tiangolo). From 68fc5fa0fdc0e8745c06de19b6ab6081bd70243b Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 24 Jun 2025 12:20:02 +0000 Subject: [PATCH 669/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 0139c9b5df..3396d5cb96 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1374](https://github.com/fastapi/sqlmodel/pull/1374) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump ruff from 0.11.13 to 0.12.0. PR [#1403](https://github.com/fastapi/sqlmodel/pull/1403) by [@dependabot[bot]](https://github.com/apps/dependabot). * ✅ Simplify tests for `tests/test_tutorial/test_code_structure/test_tutorial001.py`, one test file for multiple variants. PR [#1408](https://github.com/fastapi/sqlmodel/pull/1408) by [@tiangolo](https://github.com/tiangolo). * ✅ Simplify tests setup, one test file for multiple source variants. PR [#1407](https://github.com/fastapi/sqlmodel/pull/1407) by [@tiangolo](https://github.com/tiangolo). From 80d564b7402373e3b3a55d86f63bd3ff33e96993 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 5 Jul 2025 14:24:15 +0200 Subject: [PATCH 670/906] =?UTF-8?q?=E2=AC=86=20Bump=20ruff=20from=200.12.0?= =?UTF-8?q?=20to=200.12.2=20(#1425)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ruff](https://github.com/astral-sh/ruff) from 0.12.0 to 0.12.2. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.12.0...0.12.2) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.12.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index ee3af92244..9739caa8f4 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -3,7 +3,7 @@ pytest >=7.0.1,<9.0.0 coverage[toml] >=6.2,<8.0 mypy ==1.4.1 -ruff ==0.12.0 +ruff ==0.12.2 # For FastAPI tests fastapi >=0.103.2 httpx ==0.28.1 From 0124d30066ffffb06a76abaad0da964daef7e390 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 5 Jul 2025 14:24:25 +0200 Subject: [PATCH 671/906] =?UTF-8?q?=E2=AC=86=20Bump=20mkdocs-material=20fr?= =?UTF-8?q?om=209.6.14=20to=209.6.15=20(#1424)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.6.14 to 9.6.15. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.6.14...9.6.15) --- updated-dependencies: - dependency-name: mkdocs-material dependency-version: 9.6.15 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 849d2f89ec..1f0cdc5c3d 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,6 +1,6 @@ -e . -r requirements-docs-tests.txt -mkdocs-material==9.6.14 +mkdocs-material==9.6.15 mdx-include >=1.4.1,<2.0.0 mkdocs-redirects>=1.2.1,<1.3.0 pyyaml >=5.3.1,<7.0.0 From d9d73cd27674d751a56dcebc614fe3b221dfc37a Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 5 Jul 2025 12:24:35 +0000 Subject: [PATCH 672/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 3396d5cb96..0d73b3d7c7 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* ⬆ Bump ruff from 0.12.0 to 0.12.2. PR [#1425](https://github.com/fastapi/sqlmodel/pull/1425) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1374](https://github.com/fastapi/sqlmodel/pull/1374) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump ruff from 0.11.13 to 0.12.0. PR [#1403](https://github.com/fastapi/sqlmodel/pull/1403) by [@dependabot[bot]](https://github.com/apps/dependabot). * ✅ Simplify tests for `tests/test_tutorial/test_code_structure/test_tutorial001.py`, one test file for multiple variants. PR [#1408](https://github.com/fastapi/sqlmodel/pull/1408) by [@tiangolo](https://github.com/tiangolo). From 1d23c527f39ceedb886b1e3e43cfc6b3539af158 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 5 Jul 2025 12:24:50 +0000 Subject: [PATCH 673/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 0d73b3d7c7..aceaf4ca2c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* ⬆ Bump mkdocs-material from 9.6.14 to 9.6.15. PR [#1424](https://github.com/fastapi/sqlmodel/pull/1424) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.12.0 to 0.12.2. PR [#1425](https://github.com/fastapi/sqlmodel/pull/1425) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1374](https://github.com/fastapi/sqlmodel/pull/1374) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump ruff from 0.11.13 to 0.12.0. PR [#1403](https://github.com/fastapi/sqlmodel/pull/1403) by [@dependabot[bot]](https://github.com/apps/dependabot). From 86c29a07b98648afa73013253cf7f3689520c71e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 5 Jul 2025 14:25:08 +0200 Subject: [PATCH 674/906] =?UTF-8?q?=E2=AC=86=20Bump=20pillow=20from=2011.2?= =?UTF-8?q?.1=20to=2011.3.0=20(#1423)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [pillow](https://github.com/python-pillow/Pillow) from 11.2.1 to 11.3.0. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/11.2.1...11.3.0) --- updated-dependencies: - dependency-name: pillow dependency-version: 11.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 1f0cdc5c3d..16e18b3c0d 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -7,7 +7,7 @@ pyyaml >=5.3.1,<7.0.0 # For Material for MkDocs, Chinese search # jieba==0.42.1 # For image processing by Material for MkDocs -pillow==11.2.1 +pillow==11.3.0 # For image processing by Material for MkDocs cairosvg==2.8.2 # mkdocstrings[python]==0.25.1 From d48fc6d605f0ae5849a8d1d2a0c07ef6e5f66bfc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 5 Jul 2025 14:25:19 +0200 Subject: [PATCH 675/906] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#1418)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.12.0 → v0.12.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.0...v0.12.1) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 946d24f359..673c861279 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.0 + rev: v0.12.1 hooks: - id: ruff args: From 6bbbf6d07a02512f604767883f3e92bcd55345a6 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 5 Jul 2025 12:25:37 +0000 Subject: [PATCH 676/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index aceaf4ca2c..382d948c10 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* ⬆ Bump pillow from 11.2.1 to 11.3.0. PR [#1423](https://github.com/fastapi/sqlmodel/pull/1423) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-material from 9.6.14 to 9.6.15. PR [#1424](https://github.com/fastapi/sqlmodel/pull/1424) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.12.0 to 0.12.2. PR [#1425](https://github.com/fastapi/sqlmodel/pull/1425) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1374](https://github.com/fastapi/sqlmodel/pull/1374) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From cb26a667529a64ca5f93b90f05ce69acd37f1d79 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 5 Jul 2025 12:25:43 +0000 Subject: [PATCH 677/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 382d948c10..ae6f65fcf4 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1418](https://github.com/fastapi/sqlmodel/pull/1418) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump pillow from 11.2.1 to 11.3.0. PR [#1423](https://github.com/fastapi/sqlmodel/pull/1423) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-material from 9.6.14 to 9.6.15. PR [#1424](https://github.com/fastapi/sqlmodel/pull/1424) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.12.0 to 0.12.2. PR [#1425](https://github.com/fastapi/sqlmodel/pull/1425) by [@dependabot[bot]](https://github.com/apps/dependabot). From b6f6fbc06ad4df5e7c86a82a9e805bca7874bfb2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Jul 2025 11:28:14 +0200 Subject: [PATCH 678/906] =?UTF-8?q?=E2=AC=86=20Bump=20ruff=20from=200.12.2?= =?UTF-8?q?=20to=200.12.3=20(#1432)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ruff](https://github.com/astral-sh/ruff) from 0.12.2 to 0.12.3. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.12.2...0.12.3) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.12.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 9739caa8f4..5d7821cb79 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -3,7 +3,7 @@ pytest >=7.0.1,<9.0.0 coverage[toml] >=6.2,<8.0 mypy ==1.4.1 -ruff ==0.12.2 +ruff ==0.12.3 # For FastAPI tests fastapi >=0.103.2 httpx ==0.28.1 From fb25539f7ee2a25ae326a421137f9e6d790d1aff Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 15 Jul 2025 11:28:26 +0200 Subject: [PATCH 679/906] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#1428)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.12.1 → v0.12.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.1...v0.12.3) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 673c861279..4e8229838d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.1 + rev: v0.12.3 hooks: - id: ruff args: From b4e80bb0b38957c004fdab59e8e335ed9a2dc1d1 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 15 Jul 2025 09:28:32 +0000 Subject: [PATCH 680/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index ae6f65fcf4..b7cfd1216b 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* ⬆ Bump ruff from 0.12.2 to 0.12.3. PR [#1432](https://github.com/fastapi/sqlmodel/pull/1432) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1418](https://github.com/fastapi/sqlmodel/pull/1418) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump pillow from 11.2.1 to 11.3.0. PR [#1423](https://github.com/fastapi/sqlmodel/pull/1423) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-material from 9.6.14 to 9.6.15. PR [#1424](https://github.com/fastapi/sqlmodel/pull/1424) by [@dependabot[bot]](https://github.com/apps/dependabot). From 92e7bdbf6d96550095bf790a80fc72343cbf4900 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 15 Jul 2025 09:29:13 +0000 Subject: [PATCH 681/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index b7cfd1216b..0fe2c96587 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1428](https://github.com/fastapi/sqlmodel/pull/1428) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump ruff from 0.12.2 to 0.12.3. PR [#1432](https://github.com/fastapi/sqlmodel/pull/1432) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1418](https://github.com/fastapi/sqlmodel/pull/1418) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump pillow from 11.2.1 to 11.3.0. PR [#1423](https://github.com/fastapi/sqlmodel/pull/1423) by [@dependabot[bot]](https://github.com/apps/dependabot). From 9542f5cf19bf339da6b2b692c408093f6eba539a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Jul 2025 01:13:12 +0200 Subject: [PATCH 682/906] =?UTF-8?q?=E2=AC=86=20Bump=20ruff=20from=200.12.3?= =?UTF-8?q?=20to=200.12.4=20(#1436)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- updated-dependencies: - dependency-name: ruff dependency-version: 0.12.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 5d7821cb79..7a096cab56 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -3,7 +3,7 @@ pytest >=7.0.1,<9.0.0 coverage[toml] >=6.2,<8.0 mypy ==1.4.1 -ruff ==0.12.3 +ruff ==0.12.4 # For FastAPI tests fastapi >=0.103.2 httpx ==0.28.1 From 7f3d1754e7ed54567a3e7e8b1788fda5fd86ec4c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 24 Jul 2025 01:13:22 +0200 Subject: [PATCH 683/906] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#1437)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.12.3 → v0.12.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.3...v0.12.4) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4e8229838d..e461b01332 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.3 + rev: v0.12.4 hooks: - id: ruff args: From 86d4de5d7d4874968bf997ebabad54966011555d Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 23 Jul 2025 23:13:35 +0000 Subject: [PATCH 684/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 0fe2c96587..8df592b614 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* ⬆ Bump ruff from 0.12.3 to 0.12.4. PR [#1436](https://github.com/fastapi/sqlmodel/pull/1436) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1428](https://github.com/fastapi/sqlmodel/pull/1428) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump ruff from 0.12.2 to 0.12.3. PR [#1432](https://github.com/fastapi/sqlmodel/pull/1432) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1418](https://github.com/fastapi/sqlmodel/pull/1418) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From fee822781ff0b45e4952d8e66d024b3937aa3681 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 23 Jul 2025 23:13:42 +0000 Subject: [PATCH 685/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 8df592b614..262587aa87 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1437](https://github.com/fastapi/sqlmodel/pull/1437) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump ruff from 0.12.3 to 0.12.4. PR [#1436](https://github.com/fastapi/sqlmodel/pull/1436) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1428](https://github.com/fastapi/sqlmodel/pull/1428) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump ruff from 0.12.2 to 0.12.3. PR [#1432](https://github.com/fastapi/sqlmodel/pull/1432) by [@dependabot[bot]](https://github.com/apps/dependabot). From 7951ad296ec4eafad86a6e15980314c6bbccb325 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Aug 2025 18:11:40 +0200 Subject: [PATCH 686/906] =?UTF-8?q?=E2=AC=86=20Bump=20tiangolo/latest-chan?= =?UTF-8?q?ges=20from=200.3.2=20to=200.4.0=20(#1448)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [tiangolo/latest-changes](https://github.com/tiangolo/latest-changes) from 0.3.2 to 0.4.0. - [Release notes](https://github.com/tiangolo/latest-changes/releases) - [Commits](https://github.com/tiangolo/latest-changes/compare/0.3.2...0.4.0) --- updated-dependencies: - dependency-name: tiangolo/latest-changes dependency-version: 0.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/latest-changes.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml index d02ae1d565..5bc897726a 100644 --- a/.github/workflows/latest-changes.yml +++ b/.github/workflows/latest-changes.yml @@ -30,7 +30,7 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true - - uses: tiangolo/latest-changes@0.3.2 + - uses: tiangolo/latest-changes@0.4.0 with: token: ${{ secrets.GITHUB_TOKEN }} latest_changes_file: docs/release-notes.md From 67e926c3d18aabc87ce346b6a4db0a678079e0b1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 5 Aug 2025 16:12:12 +0000 Subject: [PATCH 687/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 262587aa87..7693f6eb3e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* ⬆ Bump tiangolo/latest-changes from 0.3.2 to 0.4.0. PR [#1448](https://github.com/fastapi/sqlmodel/pull/1448) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1437](https://github.com/fastapi/sqlmodel/pull/1437) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump ruff from 0.12.3 to 0.12.4. PR [#1436](https://github.com/fastapi/sqlmodel/pull/1436) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1428](https://github.com/fastapi/sqlmodel/pull/1428) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From f8f63bf60ca07daf8abeff0b365c2a07eff6e253 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Aug 2025 18:13:29 +0200 Subject: [PATCH 688/906] =?UTF-8?q?=E2=AC=86=20Bump=20ruff=20from=200.12.4?= =?UTF-8?q?=20to=200.12.7=20(#1447)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ruff](https://github.com/astral-sh/ruff) from 0.12.4 to 0.12.7. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.12.4...0.12.7) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.12.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 7a096cab56..9f55f9eb1e 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -3,7 +3,7 @@ pytest >=7.0.1,<9.0.0 coverage[toml] >=6.2,<8.0 mypy ==1.4.1 -ruff ==0.12.4 +ruff ==0.12.7 # For FastAPI tests fastapi >=0.103.2 httpx ==0.28.1 From 8cc6fa05f61c661ffc00751f78b0596e888f4952 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 5 Aug 2025 16:13:51 +0000 Subject: [PATCH 689/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 7693f6eb3e..d0a464bf4b 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* ⬆ Bump ruff from 0.12.4 to 0.12.7. PR [#1447](https://github.com/fastapi/sqlmodel/pull/1447) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump tiangolo/latest-changes from 0.3.2 to 0.4.0. PR [#1448](https://github.com/fastapi/sqlmodel/pull/1448) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1437](https://github.com/fastapi/sqlmodel/pull/1437) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump ruff from 0.12.3 to 0.12.4. PR [#1436](https://github.com/fastapi/sqlmodel/pull/1436) by [@dependabot[bot]](https://github.com/apps/dependabot). From 33f430e64992c59dd1a27d47ca01d47dc5f0467e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Aug 2025 18:14:53 +0200 Subject: [PATCH 690/906] =?UTF-8?q?=E2=AC=86=20Bump=20mkdocs-material=20fr?= =?UTF-8?q?om=209.6.15=20to=209.6.16=20(#1446)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.6.15 to 9.6.16. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.6.15...9.6.16) --- updated-dependencies: - dependency-name: mkdocs-material dependency-version: 9.6.16 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 16e18b3c0d..9f0c9d0be5 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,6 +1,6 @@ -e . -r requirements-docs-tests.txt -mkdocs-material==9.6.15 +mkdocs-material==9.6.16 mdx-include >=1.4.1,<2.0.0 mkdocs-redirects>=1.2.1,<1.3.0 pyyaml >=5.3.1,<7.0.0 From 03f9ac04c0a68b629ac8c0894b644112b23bc7ea Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 5 Aug 2025 16:15:15 +0000 Subject: [PATCH 691/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index d0a464bf4b..19fd1f6bd1 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* ⬆ Bump mkdocs-material from 9.6.15 to 9.6.16. PR [#1446](https://github.com/fastapi/sqlmodel/pull/1446) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.12.4 to 0.12.7. PR [#1447](https://github.com/fastapi/sqlmodel/pull/1447) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump tiangolo/latest-changes from 0.3.2 to 0.4.0. PR [#1448](https://github.com/fastapi/sqlmodel/pull/1448) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1437](https://github.com/fastapi/sqlmodel/pull/1437) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From f487de5199c36552a55d2700b5db9fcda50f99e8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 5 Aug 2025 18:17:07 +0200 Subject: [PATCH 692/906] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#1444)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.12.4 → v0.12.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.4...v0.12.7) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e461b01332..c2e6284667 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.4 + rev: v0.12.7 hooks: - id: ruff args: From e8f87e62e99831d26d15e4c938ff205d63ea3990 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 5 Aug 2025 16:17:36 +0000 Subject: [PATCH 693/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 19fd1f6bd1..fce9e97f9d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1444](https://github.com/fastapi/sqlmodel/pull/1444) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump mkdocs-material from 9.6.15 to 9.6.16. PR [#1446](https://github.com/fastapi/sqlmodel/pull/1446) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.12.4 to 0.12.7. PR [#1447](https://github.com/fastapi/sqlmodel/pull/1447) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump tiangolo/latest-changes from 0.3.2 to 0.4.0. PR [#1448](https://github.com/fastapi/sqlmodel/pull/1448) by [@dependabot[bot]](https://github.com/apps/dependabot). From 739f80fe3321f700d04ebb1e72a346c4a077b93c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 10:13:48 +0200 Subject: [PATCH 694/906] =?UTF-8?q?=E2=AC=86=20Bump=20mkdocs-material=20fr?= =?UTF-8?q?om=209.6.16=20to=209.6.17=20(#1528)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.6.16 to 9.6.17. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.6.16...9.6.17) --- updated-dependencies: - dependency-name: mkdocs-material dependency-version: 9.6.17 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 9f0c9d0be5..9a023c49cb 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,6 +1,6 @@ -e . -r requirements-docs-tests.txt -mkdocs-material==9.6.16 +mkdocs-material==9.6.17 mdx-include >=1.4.1,<2.0.0 mkdocs-redirects>=1.2.1,<1.3.0 pyyaml >=5.3.1,<7.0.0 From 905d30df8b84347baf3eed2e306bcb8a73f3e403 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 10:13:54 +0200 Subject: [PATCH 695/906] =?UTF-8?q?=E2=AC=86=20Bump=20ruff=20from=200.12.7?= =?UTF-8?q?=20to=200.12.9=20(#1521)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ruff](https://github.com/astral-sh/ruff) from 0.12.7 to 0.12.9. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.12.7...0.12.9) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.12.9 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 9f55f9eb1e..e18a8a415b 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -3,7 +3,7 @@ pytest >=7.0.1,<9.0.0 coverage[toml] >=6.2,<8.0 mypy ==1.4.1 -ruff ==0.12.7 +ruff ==0.12.9 # For FastAPI tests fastapi >=0.103.2 httpx ==0.28.1 From 740143f72d4f98d31c613af0f755a34679c0bd59 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 10:13:58 +0200 Subject: [PATCH 696/906] =?UTF-8?q?=E2=AC=86=20Bump=20mkdocs-macros-plugin?= =?UTF-8?q?=20from=201.3.7=20to=201.3.9=20(#1507)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mkdocs-macros-plugin](https://github.com/fralau/mkdocs_macros_plugin) from 1.3.7 to 1.3.9. - [Release notes](https://github.com/fralau/mkdocs_macros_plugin/releases) - [Changelog](https://github.com/fralau/mkdocs-macros-plugin/blob/master/CHANGELOG.md) - [Commits](https://github.com/fralau/mkdocs_macros_plugin/compare/v1.3.7...v1.3.9) --- updated-dependencies: - dependency-name: mkdocs-macros-plugin dependency-version: 1.3.9 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 9a023c49cb..503b86ea63 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -14,5 +14,5 @@ cairosvg==2.8.2 griffe-typingdoc==0.2.8 # For griffe, it formats with black typer == 0.16.0 -mkdocs-macros-plugin==1.3.7 +mkdocs-macros-plugin==1.3.9 markdown-include-variants==0.0.4 From b5525ecaf57b1fa6d2573edb7c13742f3ae040be Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 10:14:06 +0200 Subject: [PATCH 697/906] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#1479)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v5.0.0 → v6.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v5.0.0...v6.0.0) - [github.com/astral-sh/ruff-pre-commit: v0.12.7 → v0.12.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.7...v0.12.9) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c2e6284667..bd899fea37 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ default_language_version: python: python3.10 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: check-added-large-files - id: check-toml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.7 + rev: v0.12.9 hooks: - id: ruff args: From f4a7e6de5b6d265974a917f83a6728461cf74996 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 19 Aug 2025 08:14:10 +0000 Subject: [PATCH 698/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index fce9e97f9d..947f2ca090 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* ⬆ Bump mkdocs-material from 9.6.16 to 9.6.17. PR [#1528](https://github.com/fastapi/sqlmodel/pull/1528) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1444](https://github.com/fastapi/sqlmodel/pull/1444) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump mkdocs-material from 9.6.15 to 9.6.16. PR [#1446](https://github.com/fastapi/sqlmodel/pull/1446) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.12.4 to 0.12.7. PR [#1447](https://github.com/fastapi/sqlmodel/pull/1447) by [@dependabot[bot]](https://github.com/apps/dependabot). From 1d54dea06461e47e4f21992e8ad4b74101af19da Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 19 Aug 2025 08:14:17 +0000 Subject: [PATCH 699/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 947f2ca090..05fed3b139 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* ⬆ Bump ruff from 0.12.7 to 0.12.9. PR [#1521](https://github.com/fastapi/sqlmodel/pull/1521) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-material from 9.6.16 to 9.6.17. PR [#1528](https://github.com/fastapi/sqlmodel/pull/1528) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1444](https://github.com/fastapi/sqlmodel/pull/1444) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump mkdocs-material from 9.6.15 to 9.6.16. PR [#1446](https://github.com/fastapi/sqlmodel/pull/1446) by [@dependabot[bot]](https://github.com/apps/dependabot). From 2b02766b27bb350644fabe6d4e3864c9de24846e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 19 Aug 2025 08:14:20 +0000 Subject: [PATCH 700/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 05fed3b139..a69b3bcf2d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* ⬆ Bump mkdocs-macros-plugin from 1.3.7 to 1.3.9. PR [#1507](https://github.com/fastapi/sqlmodel/pull/1507) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.12.7 to 0.12.9. PR [#1521](https://github.com/fastapi/sqlmodel/pull/1521) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-material from 9.6.16 to 9.6.17. PR [#1528](https://github.com/fastapi/sqlmodel/pull/1528) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1444](https://github.com/fastapi/sqlmodel/pull/1444) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From daf03cd36d64ea44c3357509a5c27473210cd316 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 10:14:30 +0200 Subject: [PATCH 701/906] =?UTF-8?q?=E2=AC=86=20Bump=20actions/checkout=20f?= =?UTF-8?q?rom=204=20to=205=20(#1488)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-docs.yml | 4 ++-- .github/workflows/deploy-docs.yml | 2 +- .github/workflows/latest-changes.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/smokeshow.yml | 2 +- .github/workflows/test-redistribute.yml | 2 +- .github/workflows/test.yml | 4 ++-- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index ac1076c1ac..127566eff9 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -22,7 +22,7 @@ jobs: outputs: docs: ${{ steps.filter.outputs.docs }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # For pull requests it's not necessary to checkout the code but for the main branch it is - uses: dorny/paths-filter@v3 id: filter @@ -53,7 +53,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python uses: actions/setup-python@v5 with: diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 765a992cbd..95e238823e 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -23,7 +23,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python uses: actions/setup-python@v5 with: diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml index 5bc897726a..08802c6528 100644 --- a/.github/workflows/latest-changes.yml +++ b/.github/workflows/latest-changes.yml @@ -20,7 +20,7 @@ jobs: latest-changes: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: # To allow latest-changes to commit to the main branch token: ${{ secrets.SQLMODEL_LATEST_CHANGES }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d4ec995ddf..e7998f1067 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -22,7 +22,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python uses: actions/setup-python@v5 with: diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index 979ae0264e..2166c8e210 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -16,7 +16,7 @@ jobs: if: ${{ github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: python-version: '3.9' diff --git a/.github/workflows/test-redistribute.yml b/.github/workflows/test-redistribute.yml index 45e22bc4f3..4f8e64f585 100644 --- a/.github/workflows/test-redistribute.yml +++ b/.github/workflows/test-redistribute.yml @@ -22,7 +22,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python uses: actions/setup-python@v5 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 003c7cab17..7487c8242d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,7 +39,7 @@ jobs: fail-fast: false runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python uses: actions/setup-python@v5 with: @@ -87,7 +87,7 @@ jobs: - test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: python-version: '3.13' From 81e2a75c1df8c6863dcded2129c1e2c44c1b435d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 19 Aug 2025 08:14:36 +0000 Subject: [PATCH 702/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index a69b3bcf2d..e0e3b60b15 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1479](https://github.com/fastapi/sqlmodel/pull/1479) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump mkdocs-macros-plugin from 1.3.7 to 1.3.9. PR [#1507](https://github.com/fastapi/sqlmodel/pull/1507) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.12.7 to 0.12.9. PR [#1521](https://github.com/fastapi/sqlmodel/pull/1521) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-material from 9.6.16 to 9.6.17. PR [#1528](https://github.com/fastapi/sqlmodel/pull/1528) by [@dependabot[bot]](https://github.com/apps/dependabot). From f239249b6cc28ddf37cd7fdd98fbd148a405b251 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 10:14:44 +0200 Subject: [PATCH 703/906] =?UTF-8?q?=E2=AC=86=20Bump=20actions/download-art?= =?UTF-8?q?ifact=20from=204=20to=205=20(#1451)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 5. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/deploy-docs.yml | 2 +- .github/workflows/smokeshow.yml | 2 +- .github/workflows/test.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 95e238823e..f261979d7b 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -49,7 +49,7 @@ jobs: run: | rm -rf ./site mkdir ./site - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: path: ./site/ pattern: docs-site diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index 2166c8e210..efb979d2ea 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -29,7 +29,7 @@ jobs: requirements**.txt pyproject.toml - run: uv pip install -r requirements-github-actions.txt - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: name: coverage-html path: htmlcov diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7487c8242d..7026221265 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -100,7 +100,7 @@ jobs: requirements**.txt pyproject.toml - name: Get coverage files - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: pattern: coverage-* path: coverage From 4bfcf20b5354e34ba4b19606c07f00b3bcce7a39 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 19 Aug 2025 08:14:53 +0000 Subject: [PATCH 704/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index e0e3b60b15..fda46a0c60 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* ⬆ Bump actions/checkout from 4 to 5. PR [#1488](https://github.com/fastapi/sqlmodel/pull/1488) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1479](https://github.com/fastapi/sqlmodel/pull/1479) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump mkdocs-macros-plugin from 1.3.7 to 1.3.9. PR [#1507](https://github.com/fastapi/sqlmodel/pull/1507) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.12.7 to 0.12.9. PR [#1521](https://github.com/fastapi/sqlmodel/pull/1521) by [@dependabot[bot]](https://github.com/apps/dependabot). From eea209afbda4a01ecb7e101b5028cecf054a7fbb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 19 Aug 2025 08:15:02 +0000 Subject: [PATCH 705/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index fda46a0c60..0f68ec43e6 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* ⬆ Bump actions/download-artifact from 4 to 5. PR [#1451](https://github.com/fastapi/sqlmodel/pull/1451) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/checkout from 4 to 5. PR [#1488](https://github.com/fastapi/sqlmodel/pull/1488) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1479](https://github.com/fastapi/sqlmodel/pull/1479) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump mkdocs-macros-plugin from 1.3.7 to 1.3.9. PR [#1507](https://github.com/fastapi/sqlmodel/pull/1507) by [@dependabot[bot]](https://github.com/apps/dependabot). From 8697fe9fdf9f9646a0326d6b6accd322adfff520 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 16:35:56 +0200 Subject: [PATCH 706/906] =?UTF-8?q?=E2=AC=86=20Bump=20typer=20from=200.16.?= =?UTF-8?q?0=20to=200.16.1=20(#1531)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [typer](https://github.com/fastapi/typer) from 0.16.0 to 0.16.1. - [Release notes](https://github.com/fastapi/typer/releases) - [Changelog](https://github.com/fastapi/typer/blob/master/docs/release-notes.md) - [Commits](https://github.com/fastapi/typer/compare/0.16.0...0.16.1) --- updated-dependencies: - dependency-name: typer dependency-version: 0.16.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sofie Van Landeghem --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 503b86ea63..0b63b083ef 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -13,6 +13,6 @@ cairosvg==2.8.2 # mkdocstrings[python]==0.25.1 griffe-typingdoc==0.2.8 # For griffe, it formats with black -typer == 0.16.0 +typer == 0.16.1 mkdocs-macros-plugin==1.3.9 markdown-include-variants==0.0.4 From c5085b6349889f4d7fabedc2d5fc02224045cad5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 19 Aug 2025 14:36:17 +0000 Subject: [PATCH 707/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 0f68ec43e6..6bc0741444 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* ⬆ Bump typer from 0.16.0 to 0.16.1. PR [#1531](https://github.com/fastapi/sqlmodel/pull/1531) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/download-artifact from 4 to 5. PR [#1451](https://github.com/fastapi/sqlmodel/pull/1451) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/checkout from 4 to 5. PR [#1488](https://github.com/fastapi/sqlmodel/pull/1488) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1479](https://github.com/fastapi/sqlmodel/pull/1479) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From 48cf9727fe408f80653325c5f93d9be4e8ec96aa Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 26 Aug 2025 16:06:17 +0200 Subject: [PATCH 708/906] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#1534)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.12.9 → v0.12.10](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.9...v0.12.10) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bd899fea37..e9e299b1c8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.9 + rev: v0.12.10 hooks: - id: ruff args: From 091a795f38f1871f5de8719ee9546618ae8b45c6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Aug 2025 16:06:31 +0200 Subject: [PATCH 709/906] =?UTF-8?q?=E2=AC=86=20Bump=20ruff=20from=200.12.9?= =?UTF-8?q?=20to=200.12.10=20(#1532)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ruff](https://github.com/astral-sh/ruff) from 0.12.9 to 0.12.10. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.12.9...0.12.10) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.12.10 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index e18a8a415b..33b0c11571 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -3,7 +3,7 @@ pytest >=7.0.1,<9.0.0 coverage[toml] >=6.2,<8.0 mypy ==1.4.1 -ruff ==0.12.9 +ruff ==0.12.10 # For FastAPI tests fastapi >=0.103.2 httpx ==0.28.1 From dad82ef4fe1fd1744dc33f0e5bb3b5e02738a381 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 26 Aug 2025 14:06:37 +0000 Subject: [PATCH 710/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 6bc0741444..42d0e34917 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1534](https://github.com/fastapi/sqlmodel/pull/1534) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump typer from 0.16.0 to 0.16.1. PR [#1531](https://github.com/fastapi/sqlmodel/pull/1531) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/download-artifact from 4 to 5. PR [#1451](https://github.com/fastapi/sqlmodel/pull/1451) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/checkout from 4 to 5. PR [#1488](https://github.com/fastapi/sqlmodel/pull/1488) by [@dependabot[bot]](https://github.com/apps/dependabot). From f3186a2947d2a81c92c8fd3bf43ead5ad925c054 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 26 Aug 2025 14:06:53 +0000 Subject: [PATCH 711/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 42d0e34917..371c4298ed 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ ### Internal +* ⬆ Bump ruff from 0.12.9 to 0.12.10. PR [#1532](https://github.com/fastapi/sqlmodel/pull/1532) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1534](https://github.com/fastapi/sqlmodel/pull/1534) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump typer from 0.16.0 to 0.16.1. PR [#1531](https://github.com/fastapi/sqlmodel/pull/1531) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/download-artifact from 4 to 5. PR [#1451](https://github.com/fastapi/sqlmodel/pull/1451) by [@dependabot[bot]](https://github.com/apps/dependabot). From 8346e52495a5cd136930205c544351343310a0a4 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Wed, 27 Aug 2025 20:53:05 +0200 Subject: [PATCH 712/906] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typos=20in?= =?UTF-8?q?=20`docs/tutorial/relationship-attributes/cascade-delete-relati?= =?UTF-8?q?onships.md`=20(#1543)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix typos --- .../relationship-attributes/cascade-delete-relationships.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/tutorial/relationship-attributes/cascade-delete-relationships.md b/docs/tutorial/relationship-attributes/cascade-delete-relationships.md index 1683c9cb18..3b6fe9156c 100644 --- a/docs/tutorial/relationship-attributes/cascade-delete-relationships.md +++ b/docs/tutorial/relationship-attributes/cascade-delete-relationships.md @@ -185,7 +185,7 @@ For example, **SQLite** doesn't support them by default. They have to be manuall PRAGMA foreign_keys = ON; ``` -So, in general is a good idea to have both `cascade_delete` and `ondelete` configured. +So, in general it's a good idea to have both `cascade_delete` and `ondelete` configured. /// tip @@ -203,7 +203,7 @@ Just a note to remember... 🤓 class Hero(SQLModel, table=True): ... - team_id: int Field(foreign_key="team.id", ondelete="CASCADE") + team_id: int = Field(foreign_key="team.id", ondelete="CASCADE") ``` * `cascade_delete` is put on the `Relationship()`. Normally on the **"one"** side in "one-to-many" relationships, the side **without a foreign key**. @@ -348,7 +348,7 @@ The result would be these tables. | id | name | secret_name | age | team_id | | ---- | --------------- | ---------------- | ---- | ------- | -| 1 | Deadpond | Dive WIlson | | 1 | +| 1 | Deadpond | Dive Wilson | | 1 | | 2 | Rusty-Man | Tommy Sharp | 48 | 2 | | 3 | Spider-Boy | Pedro Parqueador | | 2 | | 4 | Black Lion | Trevor Challa | 35 | NULL | From 55abec9a9cd04f260d836726a63b96ba351ae74d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 27 Aug 2025 18:53:22 +0000 Subject: [PATCH 713/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 371c4298ed..bb3724f1c2 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Docs +* ✏️ Fix typos in `docs/tutorial/relationship-attributes/cascade-delete-relationships.md`. PR [#1543](https://github.com/fastapi/sqlmodel/pull/1543) by [@YuriiMotov](https://github.com/YuriiMotov). * 🍱 Update SVG files, a single file per diagram, sans-serif fonts. PR [#1373](https://github.com/fastapi/sqlmodel/pull/1373) by [@tiangolo](https://github.com/tiangolo). * 📝 Grammar tweak in `docs/tutorial/insert.md`. PR [#1368](https://github.com/fastapi/sqlmodel/pull/1368) by [@brettcannon](https://github.com/brettcannon). * 📝 Update `docs/tutorial/fastapi/relationships.md`. PR [#1365](https://github.com/fastapi/sqlmodel/pull/1365) by [@Foxerine](https://github.com/Foxerine). From 3b1b70cf1191c573f6fa0f6e49b74ee57378fa76 Mon Sep 17 00:00:00 2001 From: Sofie Van Landeghem Date: Fri, 5 Sep 2025 10:33:55 +0200 Subject: [PATCH 714/906] =?UTF-8?q?=F0=9F=91=B7=20Detect=20and=20label=20m?= =?UTF-8?q?erge=20conflicts=20on=20PRs=20automatically=20(#1552)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/detect-conflicts.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/detect-conflicts.yml diff --git a/.github/workflows/detect-conflicts.yml b/.github/workflows/detect-conflicts.yml new file mode 100644 index 0000000000..aba329db85 --- /dev/null +++ b/.github/workflows/detect-conflicts.yml @@ -0,0 +1,19 @@ +name: "Conflict detector" +on: + push: + pull_request_target: + types: [synchronize] + +jobs: + main: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - name: Check if PRs have merge conflicts + uses: eps1lon/actions-label-merge-conflict@v3 + with: + dirtyLabel: "conflicts" + repoToken: "${{ secrets.GITHUB_TOKEN }}" + commentOnDirty: "This pull request has a merge conflict that needs to be resolved." From 10af2b6bc8481db24eefedc4cd4d8579ee2ddc25 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 5 Sep 2025 08:35:02 +0000 Subject: [PATCH 715/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index bb3724f1c2..e7021caf5e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -20,6 +20,7 @@ ### Internal +* 👷 Detect and label merge conflicts on PRs automatically. PR [#1552](https://github.com/fastapi/sqlmodel/pull/1552) by [@svlandeg](https://github.com/svlandeg). * ⬆ Bump ruff from 0.12.9 to 0.12.10. PR [#1532](https://github.com/fastapi/sqlmodel/pull/1532) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1534](https://github.com/fastapi/sqlmodel/pull/1534) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump typer from 0.16.0 to 0.16.1. PR [#1531](https://github.com/fastapi/sqlmodel/pull/1531) by [@dependabot[bot]](https://github.com/apps/dependabot). From a3c3a463df91988dcc416e5a886da10b92be11b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 14:49:42 +0200 Subject: [PATCH 716/906] =?UTF-8?q?=E2=AC=86=20Bump=20pypa/gh-action-pypi-?= =?UTF-8?q?publish=20from=201.12.4=20to=201.13.0=20(#1550)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.12.4 to 1.13.0. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.12.4...v1.13.0) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-version: 1.13.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e7998f1067..eaf17ca550 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -34,4 +34,4 @@ jobs: TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }} run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.12.4 + uses: pypa/gh-action-pypi-publish@v1.13.0 From 59b8354c1893db6bd10f97ce26db93d0cdee1c3f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 5 Sep 2025 12:50:03 +0000 Subject: [PATCH 717/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index e7021caf5e..711ba56e2f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -20,6 +20,7 @@ ### Internal +* ⬆ Bump pypa/gh-action-pypi-publish from 1.12.4 to 1.13.0. PR [#1550](https://github.com/fastapi/sqlmodel/pull/1550) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Detect and label merge conflicts on PRs automatically. PR [#1552](https://github.com/fastapi/sqlmodel/pull/1552) by [@svlandeg](https://github.com/svlandeg). * ⬆ Bump ruff from 0.12.9 to 0.12.10. PR [#1532](https://github.com/fastapi/sqlmodel/pull/1532) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1534](https://github.com/fastapi/sqlmodel/pull/1534) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From 15c3f35082753b24c62c7c4900b5c004ee615182 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 15:09:08 +0200 Subject: [PATCH 718/906] =?UTF-8?q?=E2=AC=86=20Bump=20typer=20from=200.16.?= =?UTF-8?q?1=20to=200.17.3=20(#1547)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [typer](https://github.com/fastapi/typer) from 0.16.1 to 0.17.3. - [Release notes](https://github.com/fastapi/typer/releases) - [Changelog](https://github.com/fastapi/typer/blob/master/docs/release-notes.md) - [Commits](https://github.com/fastapi/typer/compare/0.16.1...0.17.3) --- updated-dependencies: - dependency-name: typer dependency-version: 0.17.3 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 0b63b083ef..30985d005e 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -13,6 +13,6 @@ cairosvg==2.8.2 # mkdocstrings[python]==0.25.1 griffe-typingdoc==0.2.8 # For griffe, it formats with black -typer == 0.16.1 +typer == 0.17.3 mkdocs-macros-plugin==1.3.9 markdown-include-variants==0.0.4 From e035d1c3c2ff02178730065a6a0b2ba691c49011 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 15:09:30 +0200 Subject: [PATCH 719/906] =?UTF-8?q?=E2=AC=86=20Bump=20ruff=20from=200.12.1?= =?UTF-8?q?0=20to=200.12.12=20(#1548)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ruff](https://github.com/astral-sh/ruff) from 0.12.10 to 0.12.12. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.12.10...0.12.12) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.12.12 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 33b0c11571..a5d012d4f0 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -3,7 +3,7 @@ pytest >=7.0.1,<9.0.0 coverage[toml] >=6.2,<8.0 mypy ==1.4.1 -ruff ==0.12.10 +ruff ==0.12.12 # For FastAPI tests fastapi >=0.103.2 httpx ==0.28.1 From 5565fd8fc0e9702fdc7ab87579a6bae99502fa6e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 5 Sep 2025 13:09:32 +0000 Subject: [PATCH 720/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 711ba56e2f..a996a1f4fb 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -20,6 +20,7 @@ ### Internal +* ⬆ Bump typer from 0.16.1 to 0.17.3. PR [#1547](https://github.com/fastapi/sqlmodel/pull/1547) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pypa/gh-action-pypi-publish from 1.12.4 to 1.13.0. PR [#1550](https://github.com/fastapi/sqlmodel/pull/1550) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Detect and label merge conflicts on PRs automatically. PR [#1552](https://github.com/fastapi/sqlmodel/pull/1552) by [@svlandeg](https://github.com/svlandeg). * ⬆ Bump ruff from 0.12.9 to 0.12.10. PR [#1532](https://github.com/fastapi/sqlmodel/pull/1532) by [@dependabot[bot]](https://github.com/apps/dependabot). From 7fa9d2f29295ca9e726e83b376b25cf8aec7c1d2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 15:09:41 +0200 Subject: [PATCH 721/906] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#1546)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.12.10 → v0.12.11](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.10...v0.12.11) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e9e299b1c8..8e6cabbd56 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.10 + rev: v0.12.11 hooks: - id: ruff args: From 5388e0b04accc294aa7f7eb49ef93c508ee4af70 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 5 Sep 2025 13:09:52 +0000 Subject: [PATCH 722/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index a996a1f4fb..749ac277fa 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -20,6 +20,7 @@ ### Internal +* ⬆ Bump ruff from 0.12.10 to 0.12.12. PR [#1548](https://github.com/fastapi/sqlmodel/pull/1548) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump typer from 0.16.1 to 0.17.3. PR [#1547](https://github.com/fastapi/sqlmodel/pull/1547) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pypa/gh-action-pypi-publish from 1.12.4 to 1.13.0. PR [#1550](https://github.com/fastapi/sqlmodel/pull/1550) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Detect and label merge conflicts on PRs automatically. PR [#1552](https://github.com/fastapi/sqlmodel/pull/1552) by [@svlandeg](https://github.com/svlandeg). From c8e32d4d8a567b31fffd321e8f4b8aec10242cbc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 5 Sep 2025 13:10:11 +0000 Subject: [PATCH 723/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 749ac277fa..998e432068 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -20,6 +20,7 @@ ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1546](https://github.com/fastapi/sqlmodel/pull/1546) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump ruff from 0.12.10 to 0.12.12. PR [#1548](https://github.com/fastapi/sqlmodel/pull/1548) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump typer from 0.16.1 to 0.17.3. PR [#1547](https://github.com/fastapi/sqlmodel/pull/1547) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pypa/gh-action-pypi-publish from 1.12.4 to 1.13.0. PR [#1550](https://github.com/fastapi/sqlmodel/pull/1550) by [@dependabot[bot]](https://github.com/apps/dependabot). From 213b43bea1b725a90fd1f3f2384bca15a8368e17 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 09:37:29 +0200 Subject: [PATCH 724/906] =?UTF-8?q?=E2=AC=86=20Bump=20typer=20from=200.17.?= =?UTF-8?q?3=20to=200.17.4=20(#1554)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [typer](https://github.com/fastapi/typer) from 0.17.3 to 0.17.4. - [Release notes](https://github.com/fastapi/typer/releases) - [Changelog](https://github.com/fastapi/typer/blob/master/docs/release-notes.md) - [Commits](https://github.com/fastapi/typer/compare/0.17.3...0.17.4) --- updated-dependencies: - dependency-name: typer dependency-version: 0.17.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 30985d005e..9e48b66ba7 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -13,6 +13,6 @@ cairosvg==2.8.2 # mkdocstrings[python]==0.25.1 griffe-typingdoc==0.2.8 # For griffe, it formats with black -typer == 0.17.3 +typer == 0.17.4 mkdocs-macros-plugin==1.3.9 markdown-include-variants==0.0.4 From 278a1799d7134f034982a4b24654a56633f7ac09 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 8 Sep 2025 07:37:51 +0000 Subject: [PATCH 725/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 998e432068..4c49d6d4cc 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -20,6 +20,7 @@ ### Internal +* ⬆ Bump typer from 0.17.3 to 0.17.4. PR [#1554](https://github.com/fastapi/sqlmodel/pull/1554) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1546](https://github.com/fastapi/sqlmodel/pull/1546) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump ruff from 0.12.10 to 0.12.12. PR [#1548](https://github.com/fastapi/sqlmodel/pull/1548) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump typer from 0.16.1 to 0.17.3. PR [#1547](https://github.com/fastapi/sqlmodel/pull/1547) by [@dependabot[bot]](https://github.com/apps/dependabot). From 83d52604d79b2361f082940ac158b52b569b7b55 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 13 Sep 2025 14:27:02 +0200 Subject: [PATCH 726/906] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#1556)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.12.11 → v0.12.12](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.11...v0.12.12) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8e6cabbd56..cb701e1512 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.11 + rev: v0.12.12 hooks: - id: ruff args: From 4bac03290a0a5786304df0189d44ad87a7fdc99d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 13 Sep 2025 12:27:19 +0000 Subject: [PATCH 727/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 4c49d6d4cc..3e6d626cbc 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -20,6 +20,7 @@ ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1556](https://github.com/fastapi/sqlmodel/pull/1556) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump typer from 0.17.3 to 0.17.4. PR [#1554](https://github.com/fastapi/sqlmodel/pull/1554) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1546](https://github.com/fastapi/sqlmodel/pull/1546) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump ruff from 0.12.10 to 0.12.12. PR [#1548](https://github.com/fastapi/sqlmodel/pull/1548) by [@dependabot[bot]](https://github.com/apps/dependabot). From 94f1df2ebd16b17d967fc59241d6826b0bf56ce1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Sep 2025 10:55:16 +0200 Subject: [PATCH 728/906] =?UTF-8?q?=E2=AC=86=20Bump=20actions/labeler=20fr?= =?UTF-8?q?om=205=20to=206=20(#1549)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/labeler](https://github.com/actions/labeler) from 5 to 6. - [Release notes](https://github.com/actions/labeler/releases) - [Commits](https://github.com/actions/labeler/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/labeler dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/labeler.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index e8e58015a2..7aeb448e6f 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -16,7 +16,7 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/labeler@v5 + - uses: actions/labeler@v6 if: ${{ github.event.action != 'labeled' && github.event.action != 'unlabeled' }} - run: echo "Done adding labels" # Run this after labeler applied labels From 342f32078e0f3b4c2e72b485a3d19e827ae000f5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 16 Sep 2025 08:55:36 +0000 Subject: [PATCH 729/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 3e6d626cbc..51ea7a4526 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -20,6 +20,7 @@ ### Internal +* ⬆ Bump actions/labeler from 5 to 6. PR [#1549](https://github.com/fastapi/sqlmodel/pull/1549) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1556](https://github.com/fastapi/sqlmodel/pull/1556) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump typer from 0.17.3 to 0.17.4. PR [#1554](https://github.com/fastapi/sqlmodel/pull/1554) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1546](https://github.com/fastapi/sqlmodel/pull/1546) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From 0bacb3e394ec7bc6fac465ea62dfba2b8e032b77 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 16 Sep 2025 10:55:55 +0200 Subject: [PATCH 730/906] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#1564)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.12.12 → v0.13.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.12...v0.13.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cb701e1512..94016ad31d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.12 + rev: v0.13.0 hooks: - id: ruff args: From 364ec5771b99e80a62f1885efd853023aa52543b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Sep 2025 10:56:03 +0200 Subject: [PATCH 731/906] =?UTF-8?q?=E2=AC=86=20Bump=20ruff=20from=200.12.1?= =?UTF-8?q?2=20to=200.13.0=20(#1559)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ruff](https://github.com/astral-sh/ruff) from 0.12.12 to 0.13.0. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.12.12...0.13.0) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.13.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index a5d012d4f0..dc738ea93b 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -3,7 +3,7 @@ pytest >=7.0.1,<9.0.0 coverage[toml] >=6.2,<8.0 mypy ==1.4.1 -ruff ==0.12.12 +ruff ==0.13.0 # For FastAPI tests fastapi >=0.103.2 httpx ==0.28.1 From d7d8d3af55efb2acd1022d426d48ea9ee10a1bfe Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 16 Sep 2025 08:56:10 +0000 Subject: [PATCH 732/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 51ea7a4526..9161c2806f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -20,6 +20,7 @@ ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1564](https://github.com/fastapi/sqlmodel/pull/1564) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump actions/labeler from 5 to 6. PR [#1549](https://github.com/fastapi/sqlmodel/pull/1549) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1556](https://github.com/fastapi/sqlmodel/pull/1556) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump typer from 0.17.3 to 0.17.4. PR [#1554](https://github.com/fastapi/sqlmodel/pull/1554) by [@dependabot[bot]](https://github.com/apps/dependabot). From 1b66f2a818fdd28477cebdae9bb435ba991ebab4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 16 Sep 2025 08:56:33 +0000 Subject: [PATCH 733/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 9161c2806f..3699da38b0 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -20,6 +20,7 @@ ### Internal +* ⬆ Bump ruff from 0.12.12 to 0.13.0. PR [#1559](https://github.com/fastapi/sqlmodel/pull/1559) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1564](https://github.com/fastapi/sqlmodel/pull/1564) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump actions/labeler from 5 to 6. PR [#1549](https://github.com/fastapi/sqlmodel/pull/1549) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1556](https://github.com/fastapi/sqlmodel/pull/1556) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From 6a98bf5df0cf91484fdcd6ea3743abfc975cb0af Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Sep 2025 11:02:46 +0200 Subject: [PATCH 734/906] =?UTF-8?q?=E2=AC=86=20Bump=20actions/setup-python?= =?UTF-8?q?=20from=205=20to=206=20(#1551)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-docs.yml | 2 +- .github/workflows/deploy-docs.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/smokeshow.yml | 2 +- .github/workflows/test-redistribute.yml | 2 +- .github/workflows/test.yml | 4 ++-- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 127566eff9..7426a744b3 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -55,7 +55,7 @@ jobs: run: echo "$GITHUB_CONTEXT" - uses: actions/checkout@v5 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.11" - name: Setup uv diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index f261979d7b..42d741dcf2 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -25,7 +25,7 @@ jobs: run: echo "$GITHUB_CONTEXT" - uses: actions/checkout@v5 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.11" - name: Setup uv diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index eaf17ca550..b35142f50e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -24,7 +24,7 @@ jobs: steps: - uses: actions/checkout@v5 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.11" - name: Install build dependencies diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index efb979d2ea..7861b0e27c 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: '3.9' - name: Setup uv diff --git a/.github/workflows/test-redistribute.yml b/.github/workflows/test-redistribute.yml index 4f8e64f585..2a7ff20d31 100644 --- a/.github/workflows/test-redistribute.yml +++ b/.github/workflows/test-redistribute.yml @@ -24,7 +24,7 @@ jobs: run: echo "$GITHUB_CONTEXT" - uses: actions/checkout@v5 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.10" - name: Install build dependencies diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7026221265..570941f5b9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,7 +41,7 @@ jobs: steps: - uses: actions/checkout@v5 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Setup uv @@ -88,7 +88,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: '3.13' - name: Setup uv From c7f120534c9c631e7ce248f5fd60be664b6de28f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 16 Sep 2025 09:03:05 +0000 Subject: [PATCH 735/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 3699da38b0..23dbb6560b 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -20,6 +20,7 @@ ### Internal +* ⬆ Bump actions/setup-python from 5 to 6. PR [#1551](https://github.com/fastapi/sqlmodel/pull/1551) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.12.12 to 0.13.0. PR [#1559](https://github.com/fastapi/sqlmodel/pull/1559) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1564](https://github.com/fastapi/sqlmodel/pull/1564) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump actions/labeler from 5 to 6. PR [#1549](https://github.com/fastapi/sqlmodel/pull/1549) by [@dependabot[bot]](https://github.com/apps/dependabot). From bffa05f3d8beb477b03d7f0285998923f0bddbbb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Sep 2025 15:25:11 +0200 Subject: [PATCH 736/906] =?UTF-8?q?=E2=AC=86=20Bump=20mkdocs-material=20fr?= =?UTF-8?q?om=209.6.17=20to=209.6.20=20(#1565)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.6.17 to 9.6.20. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.6.17...9.6.20) --- updated-dependencies: - dependency-name: mkdocs-material dependency-version: 9.6.20 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 9e48b66ba7..3ec8ba7294 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,6 +1,6 @@ -e . -r requirements-docs-tests.txt -mkdocs-material==9.6.17 +mkdocs-material==9.6.20 mdx-include >=1.4.1,<2.0.0 mkdocs-redirects>=1.2.1,<1.3.0 pyyaml >=5.3.1,<7.0.0 From 9277110a579bb08f7d677c44c8bf72331f150094 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 16 Sep 2025 13:25:30 +0000 Subject: [PATCH 737/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 23dbb6560b..ce13dfc020 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -20,6 +20,7 @@ ### Internal +* ⬆ Bump mkdocs-material from 9.6.17 to 9.6.20. PR [#1565](https://github.com/fastapi/sqlmodel/pull/1565) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/setup-python from 5 to 6. PR [#1551](https://github.com/fastapi/sqlmodel/pull/1551) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.12.12 to 0.13.0. PR [#1559](https://github.com/fastapi/sqlmodel/pull/1559) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1564](https://github.com/fastapi/sqlmodel/pull/1564) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From 3a11bb260dba251429bebf3189df7232471d8a05 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Sep 2025 15:25:51 +0200 Subject: [PATCH 738/906] =?UTF-8?q?=E2=AC=86=20Bump=20griffe-typingdoc=20f?= =?UTF-8?q?rom=200.2.8=20to=200.2.9=20(#1553)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [griffe-typingdoc](https://github.com/mkdocstrings/griffe-typingdoc) from 0.2.8 to 0.2.9. - [Release notes](https://github.com/mkdocstrings/griffe-typingdoc/releases) - [Changelog](https://github.com/mkdocstrings/griffe-typingdoc/blob/main/CHANGELOG.md) - [Commits](https://github.com/mkdocstrings/griffe-typingdoc/compare/0.2.8...0.2.9) --- updated-dependencies: - dependency-name: griffe-typingdoc dependency-version: 0.2.9 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 3ec8ba7294..d50164f510 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -11,7 +11,7 @@ pillow==11.3.0 # For image processing by Material for MkDocs cairosvg==2.8.2 # mkdocstrings[python]==0.25.1 -griffe-typingdoc==0.2.8 +griffe-typingdoc==0.2.9 # For griffe, it formats with black typer == 0.17.4 mkdocs-macros-plugin==1.3.9 From b8c0e164d21f3a49d8900c9f958bb514c4a3dcee Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 16 Sep 2025 13:26:12 +0000 Subject: [PATCH 739/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index ce13dfc020..e6f073af58 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -20,6 +20,7 @@ ### Internal +* ⬆ Bump griffe-typingdoc from 0.2.8 to 0.2.9. PR [#1553](https://github.com/fastapi/sqlmodel/pull/1553) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-material from 9.6.17 to 9.6.20. PR [#1565](https://github.com/fastapi/sqlmodel/pull/1565) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/setup-python from 5 to 6. PR [#1551](https://github.com/fastapi/sqlmodel/pull/1551) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.12.12 to 0.13.0. PR [#1559](https://github.com/fastapi/sqlmodel/pull/1559) by [@dependabot[bot]](https://github.com/apps/dependabot). From 037c051997306cf0c5550cda1e7630cdebcfdfec Mon Sep 17 00:00:00 2001 From: seria Date: Thu, 18 Sep 2025 05:37:02 +0800 Subject: [PATCH 740/906] =?UTF-8?q?=E2=9C=A8=20Add=20overload=20for=20`exe?= =?UTF-8?q?c`=20method=20to=20support=20`insert`,=20`update`,=20`delete`?= =?UTF-8?q?=20statements=20(#1342)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sofie Van Landeghem Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> --- sqlmodel/ext/asyncio/session.py | 19 ++++++++++++++++++- sqlmodel/orm/session.py | 19 ++++++++++++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/sqlmodel/ext/asyncio/session.py b/sqlmodel/ext/asyncio/session.py index 467d0bd84e..54488357bb 100644 --- a/sqlmodel/ext/asyncio/session.py +++ b/sqlmodel/ext/asyncio/session.py @@ -12,6 +12,7 @@ ) from sqlalchemy import util +from sqlalchemy.engine.cursor import CursorResult from sqlalchemy.engine.interfaces import _CoreAnyExecuteParams from sqlalchemy.engine.result import Result, ScalarResult, TupleResult from sqlalchemy.ext.asyncio import AsyncSession as _AsyncSession @@ -19,6 +20,7 @@ from sqlalchemy.ext.asyncio.session import _EXECUTE_OPTIONS from sqlalchemy.orm._typing import OrmExecuteOptionsParameter from sqlalchemy.sql.base import Executable as _Executable +from sqlalchemy.sql.dml import UpdateBase from sqlalchemy.util.concurrency import greenlet_spawn from typing_extensions import deprecated @@ -57,12 +59,25 @@ async def exec( _add_event: Optional[Any] = None, ) -> ScalarResult[_TSelectParam]: ... + @overload + async def exec( + self, + statement: UpdateBase, + *, + params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, + execution_options: Mapping[str, Any] = util.EMPTY_DICT, + bind_arguments: Optional[Dict[str, Any]] = None, + _parent_execute_state: Optional[Any] = None, + _add_event: Optional[Any] = None, + ) -> CursorResult[Any]: ... + async def exec( self, statement: Union[ Select[_TSelectParam], SelectOfScalar[_TSelectParam], Executable[_TSelectParam], + UpdateBase, ], *, params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, @@ -70,7 +85,9 @@ async def exec( bind_arguments: Optional[Dict[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, - ) -> Union[TupleResult[_TSelectParam], ScalarResult[_TSelectParam]]: + ) -> Union[ + TupleResult[_TSelectParam], ScalarResult[_TSelectParam], CursorResult[Any] + ]: if execution_options: execution_options = util.immutabledict(execution_options).union( _EXECUTE_OPTIONS diff --git a/sqlmodel/orm/session.py b/sqlmodel/orm/session.py index b60875095b..dca4733d61 100644 --- a/sqlmodel/orm/session.py +++ b/sqlmodel/orm/session.py @@ -10,6 +10,7 @@ ) from sqlalchemy import util +from sqlalchemy.engine.cursor import CursorResult from sqlalchemy.engine.interfaces import _CoreAnyExecuteParams from sqlalchemy.engine.result import Result, ScalarResult, TupleResult from sqlalchemy.orm import Query as _Query @@ -17,6 +18,7 @@ from sqlalchemy.orm._typing import OrmExecuteOptionsParameter from sqlalchemy.sql._typing import _ColumnsClauseArgument from sqlalchemy.sql.base import Executable as _Executable +from sqlalchemy.sql.dml import UpdateBase from sqlmodel.sql.base import Executable from sqlmodel.sql.expression import Select, SelectOfScalar from typing_extensions import deprecated @@ -49,12 +51,25 @@ def exec( _add_event: Optional[Any] = None, ) -> ScalarResult[_TSelectParam]: ... + @overload + def exec( + self, + statement: UpdateBase, + *, + params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, + execution_options: Mapping[str, Any] = util.EMPTY_DICT, + bind_arguments: Optional[Dict[str, Any]] = None, + _parent_execute_state: Optional[Any] = None, + _add_event: Optional[Any] = None, + ) -> CursorResult[Any]: ... + def exec( self, statement: Union[ Select[_TSelectParam], SelectOfScalar[_TSelectParam], Executable[_TSelectParam], + UpdateBase, ], *, params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, @@ -62,7 +77,9 @@ def exec( bind_arguments: Optional[Dict[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, - ) -> Union[TupleResult[_TSelectParam], ScalarResult[_TSelectParam]]: + ) -> Union[ + TupleResult[_TSelectParam], ScalarResult[_TSelectParam], CursorResult[Any] + ]: results = super().execute( statement, params=params, From de278dfdce6bbdf9ab198b53a7e3a4eff6d5c838 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 17 Sep 2025 21:37:21 +0000 Subject: [PATCH 741/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index e6f073af58..3e634ceeb2 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Features + +* ✨ Add overload for `exec` method to support `insert`, `update`, `delete` statements. PR [#1342](https://github.com/fastapi/sqlmodel/pull/1342) by [@seriaati](https://github.com/seriaati). + ### Upgrades * ⬆️ Drop support for Python 3.7, require Python 3.8 or above. PR [#1316](https://github.com/fastapi/sqlmodel/pull/1316) by [@svlandeg](https://github.com/svlandeg). From 192ba90b1e627bd107f14c98f8c96b6951e68d36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 17 Sep 2025 23:39:08 +0200 Subject: [PATCH 742/906] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.0.?= =?UTF-8?q?25?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 2 ++ sqlmodel/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 3e634ceeb2..a502f2e397 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest Changes +## 0.0.25 + ### Features * ✨ Add overload for `exec` method to support `insert`, `update`, `delete` statements. PR [#1342](https://github.com/fastapi/sqlmodel/pull/1342) by [@seriaati](https://github.com/seriaati). diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index 28bb305237..aec97b7eb9 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.0.24" +__version__ = "0.0.25" # Re-export from SQLAlchemy from sqlalchemy.engine import create_engine as create_engine From 35c0005f441cb6b2e34fa1ff45a6c73e721a14ac Mon Sep 17 00:00:00 2001 From: Sofie Van Landeghem Date: Thu, 18 Sep 2025 00:19:41 +0200 Subject: [PATCH 743/906] =?UTF-8?q?=F0=9F=92=9A=20Fix=20CI=20test=20suite?= =?UTF-8?q?=20for=20Windows=20and=20MacOS=20(#1307)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 31 ++++++++++++++++++++++--------- pyproject.toml | 1 + tests/test_select_gen.py | 2 +- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 570941f5b9..c4fdb911a8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,17 +25,30 @@ jobs: test: strategy: matrix: - os: [ ubuntu-latest ] - python-version: - - "3.8" - - "3.9" - - "3.10" - - "3.11" - - "3.12" - - "3.13" + os: [ ubuntu-latest, windows-latest, macos-latest ] + python-version: [ "3.13" ] pydantic-version: - pydantic-v1 - pydantic-v2 + include: + - os: macos-latest + python-version: "3.8" + pydantic-version: pydantic-v1 + - os: windows-latest + python-version: "3.9" + pydantic-version: pydantic-v2 + - os: ubuntu-latest + python-version: "3.10" + pydantic-version: pydantic-v1 + - os: macos-latest + python-version: "3.11" + pydantic-version: pydantic-v2 + - os: windows-latest + python-version: "3.12" + pydantic-version: pydantic-v1 + - os: ubuntu-latest + python-version: "3.12" + pydantic-version: pydantic-v2 fail-fast: false runs-on: ${{ matrix.os }} steps: @@ -78,7 +91,7 @@ jobs: - name: Store coverage files uses: actions/upload-artifact@v4 with: - name: coverage-${{ matrix.python-version }}-${{ matrix.pydantic-version }} + name: coverage-${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.pydantic-version }} path: coverage include-hidden-files: true diff --git a/pyproject.toml b/pyproject.toml index 766b055819..4ae195ac73 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,6 +81,7 @@ source = [ ] context = '${CONTEXT}' dynamic_context = "test_function" +relative_files = true [tool.coverage.report] show_missing = true diff --git a/tests/test_select_gen.py b/tests/test_select_gen.py index e14200d513..664cebf2a7 100644 --- a/tests/test_select_gen.py +++ b/tests/test_select_gen.py @@ -13,7 +13,7 @@ def test_select_gen() -> None: env = os.environ.copy() env["CHECK_JINJA"] = "1" result = subprocess.run( - [sys.executable, "scripts/generate_select.py"], + [sys.executable, Path("scripts") / "generate_select.py"], env=env, check=True, cwd=root_path, From c6acc1fa4c9ed5edb686d87eb1fd59e4f8930e3c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 17 Sep 2025 22:20:02 +0000 Subject: [PATCH 744/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index a502f2e397..43a25e1002 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Internal + +* 💚 Fix CI test suite for Windows and MacOS. PR [#1307](https://github.com/fastapi/sqlmodel/pull/1307) by [@svlandeg](https://github.com/svlandeg). + ## 0.0.25 ### Features From 5a1862ef04345a9d7d3fd480ca838b4bf799fdeb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 Sep 2025 14:56:21 +0200 Subject: [PATCH 745/906] =?UTF-8?q?=E2=AC=86=20Bump=20ruff=20from=200.13.0?= =?UTF-8?q?=20to=200.13.2=20(#1576)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ruff](https://github.com/astral-sh/ruff) from 0.13.0 to 0.13.2. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.13.0...0.13.2) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.13.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index dc738ea93b..358c093339 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -3,7 +3,7 @@ pytest >=7.0.1,<9.0.0 coverage[toml] >=6.2,<8.0 mypy ==1.4.1 -ruff ==0.13.0 +ruff ==0.13.2 # For FastAPI tests fastapi >=0.103.2 httpx ==0.28.1 From 07afd33e73e2ab37da8dabff239f015de03c6fca Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 26 Sep 2025 14:56:28 +0200 Subject: [PATCH 746/906] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#1571)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.13.0 → v0.13.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.13.0...v0.13.1) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 94016ad31d..8b24c8c0cd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.13.0 + rev: v0.13.1 hooks: - id: ruff args: From 94b5aaa7917a0260e025d1956e66c8a621fbdbb2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Sep 2025 12:56:38 +0000 Subject: [PATCH 747/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 43a25e1002..eb06485892 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Internal +* ⬆ Bump ruff from 0.13.0 to 0.13.2. PR [#1576](https://github.com/fastapi/sqlmodel/pull/1576) by [@dependabot[bot]](https://github.com/apps/dependabot). * 💚 Fix CI test suite for Windows and MacOS. PR [#1307](https://github.com/fastapi/sqlmodel/pull/1307) by [@svlandeg](https://github.com/svlandeg). ## 0.0.25 From b44642f6c6858f43671d912a52f6852a1cca642f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 Sep 2025 14:56:50 +0200 Subject: [PATCH 748/906] =?UTF-8?q?=E2=AC=86=20Bump=20typer=20from=200.17.?= =?UTF-8?q?4=20to=200.19.2=20(#1573)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [typer](https://github.com/fastapi/typer) from 0.17.4 to 0.19.2. - [Release notes](https://github.com/fastapi/typer/releases) - [Changelog](https://github.com/fastapi/typer/blob/master/docs/release-notes.md) - [Commits](https://github.com/fastapi/typer/compare/0.17.4...0.19.2) --- updated-dependencies: - dependency-name: typer dependency-version: 0.19.2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index d50164f510..bbc80dbcb5 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -13,6 +13,6 @@ cairosvg==2.8.2 # mkdocstrings[python]==0.25.1 griffe-typingdoc==0.2.9 # For griffe, it formats with black -typer == 0.17.4 +typer == 0.19.2 mkdocs-macros-plugin==1.3.9 markdown-include-variants==0.0.4 From 2bb01811711902ac5f40aa6f60b08a993157c934 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Sep 2025 12:57:34 +0000 Subject: [PATCH 749/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index eb06485892..d046ae5cb2 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Internal +* ⬆ Bump typer from 0.17.4 to 0.19.2. PR [#1573](https://github.com/fastapi/sqlmodel/pull/1573) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.13.0 to 0.13.2. PR [#1576](https://github.com/fastapi/sqlmodel/pull/1576) by [@dependabot[bot]](https://github.com/apps/dependabot). * 💚 Fix CI test suite for Windows and MacOS. PR [#1307](https://github.com/fastapi/sqlmodel/pull/1307) by [@svlandeg](https://github.com/svlandeg). From 935d896ed64999200cb98ae15e5084c00c1001bf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Sep 2025 12:57:43 +0000 Subject: [PATCH 750/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index d046ae5cb2..71d2e3a306 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1571](https://github.com/fastapi/sqlmodel/pull/1571) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump typer from 0.17.4 to 0.19.2. PR [#1573](https://github.com/fastapi/sqlmodel/pull/1573) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.13.0 to 0.13.2. PR [#1576](https://github.com/fastapi/sqlmodel/pull/1576) by [@dependabot[bot]](https://github.com/apps/dependabot). * 💚 Fix CI test suite for Windows and MacOS. PR [#1307](https://github.com/fastapi/sqlmodel/pull/1307) by [@svlandeg](https://github.com/svlandeg). From 1faefe84f8b692f63aa10133e9ae8924f06d1d94 Mon Sep 17 00:00:00 2001 From: Sofie Van Landeghem Date: Fri, 26 Sep 2025 15:17:55 +0200 Subject: [PATCH 751/906] =?UTF-8?q?=E2=AC=86=20Bump=20typing-extensions=20?= =?UTF-8?q?from=204.13.2=20to=204.15.0=20for=20Python=203.9+=20(#1580)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * bump typing-extensions to 4.15.0 * keep older version for Python 3.8 * fix --- requirements-tests.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 358c093339..f6e826511d 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -9,4 +9,6 @@ fastapi >=0.103.2 httpx ==0.28.1 dirty-equals ==0.9.0 jinja2 ==3.1.6 -typing-extensions ==4.13.2 +# Remove when support for Python 3.8 is dropped +typing-extensions ==4.13.2; python_version < "3.9" +typing-extensions ==4.15.0; python_version >= "3.9" From fd3f680c7507e866b3de356e304cba8e299acfb2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Sep 2025 13:18:13 +0000 Subject: [PATCH 752/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 71d2e3a306..0779c14565 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Internal +* ⬆ Bump typing-extensions from 4.13.2 to 4.15.0 for Python 3.9+. PR [#1580](https://github.com/fastapi/sqlmodel/pull/1580) by [@svlandeg](https://github.com/svlandeg). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1571](https://github.com/fastapi/sqlmodel/pull/1571) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump typer from 0.17.4 to 0.19.2. PR [#1573](https://github.com/fastapi/sqlmodel/pull/1573) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.13.0 to 0.13.2. PR [#1576](https://github.com/fastapi/sqlmodel/pull/1576) by [@dependabot[bot]](https://github.com/apps/dependabot). From 679bf3f1e5772a58aa22760cac542f8e70adf261 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 14:03:08 +0200 Subject: [PATCH 753/906] =?UTF-8?q?=E2=AC=86=20Bump=20markdown-include-var?= =?UTF-8?q?iants=20from=200.0.4=20to=200.0.5=20(#1582)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [markdown-include-variants](https://github.com/tiangolo/markdown-include-variants) from 0.0.4 to 0.0.5. - [Release notes](https://github.com/tiangolo/markdown-include-variants/releases) - [Changelog](https://github.com/tiangolo/markdown-include-variants/blob/main/release-notes.md) - [Commits](https://github.com/tiangolo/markdown-include-variants/compare/0.0.4...0.0.5) --- updated-dependencies: - dependency-name: markdown-include-variants dependency-version: 0.0.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index bbc80dbcb5..7c1480df88 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -15,4 +15,4 @@ griffe-typingdoc==0.2.9 # For griffe, it formats with black typer == 0.19.2 mkdocs-macros-plugin==1.3.9 -markdown-include-variants==0.0.4 +markdown-include-variants==0.0.5 From ab79c22a4de80ed544be7a13d74cf07c6fb134ce Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 29 Sep 2025 12:03:27 +0000 Subject: [PATCH 754/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 0779c14565..d5d913a9a3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Internal +* ⬆ Bump markdown-include-variants from 0.0.4 to 0.0.5. PR [#1582](https://github.com/fastapi/sqlmodel/pull/1582) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump typing-extensions from 4.13.2 to 4.15.0 for Python 3.9+. PR [#1580](https://github.com/fastapi/sqlmodel/pull/1580) by [@svlandeg](https://github.com/svlandeg). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1571](https://github.com/fastapi/sqlmodel/pull/1571) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump typer from 0.17.4 to 0.19.2. PR [#1573](https://github.com/fastapi/sqlmodel/pull/1573) by [@dependabot[bot]](https://github.com/apps/dependabot). From 6e3bae04186f5b8df15f86c395a048269a733dd0 Mon Sep 17 00:00:00 2001 From: Kofi Kusi Appau Date: Mon, 29 Sep 2025 16:08:08 +0000 Subject: [PATCH 755/906] =?UTF-8?q?=F0=9F=93=9D=20Fix=20typo=20in=20`docs/?= =?UTF-8?q?tutorial/fastapi/simple-hero-api.md`=20(#1583)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 📝 Update function name from create_tables to create_db_and_tables --- docs/tutorial/fastapi/simple-hero-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/fastapi/simple-hero-api.md b/docs/tutorial/fastapi/simple-hero-api.md index 163fa281b1..79cf075e1b 100644 --- a/docs/tutorial/fastapi/simple-hero-api.md +++ b/docs/tutorial/fastapi/simple-hero-api.md @@ -60,7 +60,7 @@ And then create an `app` object that is an instance of that `FastAPI` class: ## Create Database and Tables on `startup` -We want to make sure that once the app starts running, the function `create_tables` is called. To create the database and tables. +We want to make sure that once the app starts running, the function `create_db_and_tables` is called. To create the database and tables. This should be called only once at startup, not before every request, so we put it in the function to handle the `"startup"` event: From 528953212adee07a500ab36d01ab4eb34ca52162 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 29 Sep 2025 16:08:32 +0000 Subject: [PATCH 756/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index d5d913a9a3..f758a1ab77 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Docs + +* 📝 Fix typo in `docs/tutorial/fastapi/simple-hero-api.md`. PR [#1583](https://github.com/fastapi/sqlmodel/pull/1583) by [@kofi-kusi](https://github.com/kofi-kusi). + ### Internal * ⬆ Bump markdown-include-variants from 0.0.4 to 0.0.5. PR [#1582](https://github.com/fastapi/sqlmodel/pull/1582) by [@dependabot[bot]](https://github.com/apps/dependabot). From 44ad9bd078bdf5cf0dbd8ea7bc1ac4833829e487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 30 Sep 2025 15:29:31 +0900 Subject: [PATCH 757/906] =?UTF-8?q?=F0=9F=91=B7=20Update=20docs=20previews?= =?UTF-8?q?=20comment,=20single=20comment,=20add=20failure=20status=20(#15?= =?UTF-8?q?86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-docs.yml | 12 ++++++-- scripts/deploy_docs_status.py | 48 +++++++++++++++++++++++-------- 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 42d741dcf2..c6f57dbb64 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -44,7 +44,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} COMMIT_SHA: ${{ github.event.workflow_run.head_sha }} RUN_ID: ${{ github.run_id }} - + STATE: "pending" - name: Clean site run: | rm -rf ./site @@ -68,6 +68,14 @@ jobs: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} command: pages deploy ./site --project-name=${{ env.PROJECT_NAME }} --branch=${{ env.BRANCH }} + - name: Deploy Docs Status Error + if: failure() + run: python ./scripts/deploy_docs_status.py + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMMIT_SHA: ${{ github.event.workflow_run.head_sha }} + RUN_ID: ${{ github.run_id }} + STATE: "error" - name: Comment Deploy run: python ./scripts/deploy_docs_status.py env: @@ -75,4 +83,4 @@ jobs: DEPLOY_URL: ${{ steps.deploy.outputs.deployment-url }} COMMIT_SHA: ${{ github.event.workflow_run.head_sha }} RUN_ID: ${{ github.run_id }} - IS_DONE: "true" + STATE: "success" diff --git a/scripts/deploy_docs_status.py b/scripts/deploy_docs_status.py index c107db9463..635711bdbf 100644 --- a/scripts/deploy_docs_status.py +++ b/scripts/deploy_docs_status.py @@ -1,7 +1,8 @@ import logging import re +from typing import Literal -from github import Github +from github import Auth, Github from pydantic import BaseModel, SecretStr from pydantic_settings import BaseSettings @@ -14,7 +15,7 @@ class Settings(BaseSettings): deploy_url: str | None = None commit_sha: str run_id: int - is_done: bool = False + state: Literal["pending", "success", "error"] = "pending" class LinkData(BaseModel): @@ -27,7 +28,7 @@ def main() -> None: settings = Settings() logging.info(f"Using config: {settings.model_dump_json()}") - g = Github(settings.github_token.get_secret_value()) + g = Github(auth=Auth.Token(settings.github_token.get_secret_value())) repo = g.get_repo(settings.github_repository) use_pr = next( (pr for pr in repo.get_pulls() if pr.head.sha == settings.commit_sha), None @@ -38,24 +39,35 @@ def main() -> None: commits = list(use_pr.get_commits()) current_commit = [c for c in commits if c.sha == settings.commit_sha][0] run_url = f"https://github.com/{settings.github_repository}/actions/runs/{settings.run_id}" - if settings.is_done and not settings.deploy_url: + if settings.state == "pending": current_commit.create_status( - state="success", - description="No Docs Changes", + state="pending", + description="Deploying Docs", context="deploy-docs", target_url=run_url, ) - logging.info("No docs changes found") + logging.info("No deploy URL available yet") return + if settings.state == "error": + current_commit.create_status( + state="error", + description="Error Deploying Docs", + context="deploy-docs", + target_url=run_url, + ) + logging.info("Error deploying docs") + return + assert settings.state == "success" if not settings.deploy_url: current_commit.create_status( - state="pending", - description="Deploying Docs", + state="success", + description="No Docs Changes", context="deploy-docs", target_url=run_url, ) - logging.info("No deploy URL available yet") + logging.info("No docs changes found") return + assert settings.deploy_url current_commit.create_status( state="success", description="Docs Deployed", @@ -84,7 +96,9 @@ def main() -> None: links.append(link) links.sort(key=lambda x: x.preview_link) - message = f"📝 Docs preview for commit {settings.commit_sha} at: {deploy_url}" + header = "## 📝 Docs preview" + message = header + message += f"\n\nLast commit {settings.commit_sha} at: {deploy_url}" if links: message += "\n\n### Modified Pages\n\n" @@ -94,7 +108,17 @@ def main() -> None: message += "\n" print(message) - use_pr.as_issue().create_comment(message) + issue = use_pr.as_issue() + comments = list(issue.get_comments()) + for comment in comments: + if ( + comment.body.startswith(header) + and comment.user.login == "github-actions[bot]" + ): + comment.edit(message) + break + else: + issue.create_comment(message) logging.info("Finished") From 69b20188bdfec20bcda11c2b73be3e3d571047a8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 30 Sep 2025 06:29:46 +0000 Subject: [PATCH 758/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index f758a1ab77..d7ae850dce 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* 👷 Update docs previews comment, single comment, add failure status. PR [#1586](https://github.com/fastapi/sqlmodel/pull/1586) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump markdown-include-variants from 0.0.4 to 0.0.5. PR [#1582](https://github.com/fastapi/sqlmodel/pull/1582) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump typing-extensions from 4.13.2 to 4.15.0 for Python 3.9+. PR [#1580](https://github.com/fastapi/sqlmodel/pull/1580) by [@svlandeg](https://github.com/svlandeg). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1571](https://github.com/fastapi/sqlmodel/pull/1571) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From a289ba8cd15160e14cddd03ae8b4e754d4a97574 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 07:07:35 +0200 Subject: [PATCH 759/906] =?UTF-8?q?=E2=AC=86=20Bump=20tiangolo/issue-manag?= =?UTF-8?q?er=20from=200.5.1=20to=200.6.0=20(#1589)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [tiangolo/issue-manager](https://github.com/tiangolo/issue-manager) from 0.5.1 to 0.6.0. - [Release notes](https://github.com/tiangolo/issue-manager/releases) - [Commits](https://github.com/tiangolo/issue-manager/compare/0.5.1...0.6.0) --- updated-dependencies: - dependency-name: tiangolo/issue-manager dependency-version: 0.6.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/issue-manager.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml index 0737b40a8f..90d3d5fd4f 100644 --- a/.github/workflows/issue-manager.yml +++ b/.github/workflows/issue-manager.yml @@ -27,7 +27,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: tiangolo/issue-manager@0.5.1 + - uses: tiangolo/issue-manager@0.6.0 with: token: ${{ secrets.GITHUB_TOKEN }} config: > From 12fdacd655b5e6b0c6d2374015e0f014a3c0940d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 2 Oct 2025 05:07:56 +0000 Subject: [PATCH 760/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index d7ae850dce..d8805dc151 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* ⬆ Bump tiangolo/issue-manager from 0.5.1 to 0.6.0. PR [#1589](https://github.com/fastapi/sqlmodel/pull/1589) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Update docs previews comment, single comment, add failure status. PR [#1586](https://github.com/fastapi/sqlmodel/pull/1586) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump markdown-include-variants from 0.0.4 to 0.0.5. PR [#1582](https://github.com/fastapi/sqlmodel/pull/1582) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump typing-extensions from 4.13.2 to 4.15.0 for Python 3.9+. PR [#1580](https://github.com/fastapi/sqlmodel/pull/1580) by [@svlandeg](https://github.com/svlandeg). From 4787b687260ec5a7283aaaa23a4c90a711382bdf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 10:01:48 +0200 Subject: [PATCH 761/906] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#1584)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.13.1 → v0.13.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.13.1...v0.13.3) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8b24c8c0cd..9c075f68ea 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.13.1 + rev: v0.13.3 hooks: - id: ruff args: From 0052e986e38521831d427a41bafd907eab3688c5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 7 Oct 2025 08:02:07 +0000 Subject: [PATCH 762/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index d8805dc151..a00f1a79e8 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1584](https://github.com/fastapi/sqlmodel/pull/1584) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump tiangolo/issue-manager from 0.5.1 to 0.6.0. PR [#1589](https://github.com/fastapi/sqlmodel/pull/1589) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Update docs previews comment, single comment, add failure status. PR [#1586](https://github.com/fastapi/sqlmodel/pull/1586) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump markdown-include-variants from 0.0.4 to 0.0.5. PR [#1582](https://github.com/fastapi/sqlmodel/pull/1582) by [@dependabot[bot]](https://github.com/apps/dependabot). From d736aab755faa6065f2805f0069565b552704bb5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 10:02:34 +0200 Subject: [PATCH 763/906] =?UTF-8?q?=E2=AC=86=20Bump=20mkdocs-material=20fr?= =?UTF-8?q?om=209.6.20=20to=209.6.21=20(#1588)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.6.20 to 9.6.21. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.6.20...9.6.21) --- updated-dependencies: - dependency-name: mkdocs-material dependency-version: 9.6.21 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 7c1480df88..ccaf073e08 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,6 +1,6 @@ -e . -r requirements-docs-tests.txt -mkdocs-material==9.6.20 +mkdocs-material==9.6.21 mdx-include >=1.4.1,<2.0.0 mkdocs-redirects>=1.2.1,<1.3.0 pyyaml >=5.3.1,<7.0.0 From c1974418594de517982a29b6188e33c40e51dc99 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 7 Oct 2025 08:02:53 +0000 Subject: [PATCH 764/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index a00f1a79e8..9ca5a6907f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* ⬆ Bump mkdocs-material from 9.6.20 to 9.6.21. PR [#1588](https://github.com/fastapi/sqlmodel/pull/1588) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1584](https://github.com/fastapi/sqlmodel/pull/1584) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump tiangolo/issue-manager from 0.5.1 to 0.6.0. PR [#1589](https://github.com/fastapi/sqlmodel/pull/1589) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Update docs previews comment, single comment, add failure status. PR [#1586](https://github.com/fastapi/sqlmodel/pull/1586) by [@tiangolo](https://github.com/tiangolo). From a531c0c12d1188e5cbd80d476db4e026bb24bd1d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 10:07:25 +0200 Subject: [PATCH 765/906] =?UTF-8?q?=E2=AC=86=20Bump=20mkdocs-macros-plugin?= =?UTF-8?q?=20from=201.3.9=20to=201.4.0=20(#1581)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mkdocs-macros-plugin](https://github.com/fralau/mkdocs_macros_plugin) from 1.3.9 to 1.4.0. - [Release notes](https://github.com/fralau/mkdocs_macros_plugin/releases) - [Changelog](https://github.com/fralau/mkdocs-macros-plugin/blob/master/CHANGELOG.md) - [Commits](https://github.com/fralau/mkdocs_macros_plugin/compare/v1.3.9...v1.4.0) --- updated-dependencies: - dependency-name: mkdocs-macros-plugin dependency-version: 1.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index ccaf073e08..b6479d582c 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -14,5 +14,5 @@ cairosvg==2.8.2 griffe-typingdoc==0.2.9 # For griffe, it formats with black typer == 0.19.2 -mkdocs-macros-plugin==1.3.9 +mkdocs-macros-plugin==1.4.0 markdown-include-variants==0.0.5 From 39d8c2c0dfe183ef1e1c1aa9ecd03674b05f62c0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 7 Oct 2025 08:07:46 +0000 Subject: [PATCH 766/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 9ca5a6907f..6bb42c0aae 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* ⬆ Bump mkdocs-macros-plugin from 1.3.9 to 1.4.0. PR [#1581](https://github.com/fastapi/sqlmodel/pull/1581) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-material from 9.6.20 to 9.6.21. PR [#1588](https://github.com/fastapi/sqlmodel/pull/1588) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1584](https://github.com/fastapi/sqlmodel/pull/1584) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump tiangolo/issue-manager from 0.5.1 to 0.6.0. PR [#1589](https://github.com/fastapi/sqlmodel/pull/1589) by [@dependabot[bot]](https://github.com/apps/dependabot). From 0402496be2c24231ae002afc58f91da4c00d1c43 Mon Sep 17 00:00:00 2001 From: Andrew Grangaard Date: Wed, 8 Oct 2025 03:33:47 -0700 Subject: [PATCH 767/906] =?UTF-8?q?=F0=9F=90=9B=20Fix=20attribute=20handli?= =?UTF-8?q?ng=20in=20`model=5Fdump`=20for=20compatibility=20with=20the=20l?= =?UTF-8?q?atest=20Pydantic=20versions=20(#1595)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/main.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 38c85915aa..1925a48f04 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -109,7 +109,8 @@ def __dataclass_transform__( return lambda a: a -class FieldInfo(PydanticFieldInfo): +class FieldInfo(PydanticFieldInfo): # type: ignore[misc] + # mypy - ignore that PydanticFieldInfo is @final def __init__(self, default: Any = Undefined, **kwargs: Any) -> None: primary_key = kwargs.pop("primary_key", False) nullable = kwargs.pop("nullable", Undefined) @@ -863,27 +864,27 @@ def model_dump( mode: Union[Literal["json", "python"], str] = "python", include: Union[IncEx, None] = None, exclude: Union[IncEx, None] = None, - context: Union[Any, None] = None, + context: Union[Any, None] = None, # v2.7 by_alias: Union[bool, None] = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, + exclude_computed_fields: bool = False, # v2.12 round_trip: bool = False, warnings: Union[bool, Literal["none", "warn", "error"]] = True, - fallback: Union[Callable[[Any], Any], None] = None, - serialize_as_any: bool = False, + fallback: Union[Callable[[Any], Any], None] = None, # v2.11 + serialize_as_any: bool = False, # v2.7 ) -> Dict[str, Any]: if PYDANTIC_MINOR_VERSION < (2, 11): by_alias = by_alias or False + extra_kwargs: Dict[str, Any] = {} if PYDANTIC_MINOR_VERSION >= (2, 7): - extra_kwargs: Dict[str, Any] = { - "context": context, - "serialize_as_any": serialize_as_any, - } + extra_kwargs["context"] = context + extra_kwargs["serialize_as_any"] = serialize_as_any if PYDANTIC_MINOR_VERSION >= (2, 11): extra_kwargs["fallback"] = fallback - else: - extra_kwargs = {} + if PYDANTIC_MINOR_VERSION >= (2, 12): + extra_kwargs["exclude_computed_fields"] = exclude_computed_fields if IS_PYDANTIC_V2: return super().model_dump( mode=mode, From b7fd32d5ebdbc31f8130500840a3e48679eb5cbc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 8 Oct 2025 10:34:13 +0000 Subject: [PATCH 768/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 6bb42c0aae..42e193641b 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Fixes + +* 🐛 Fix attribute handling in `model_dump` for compatibility with the latest Pydantic versions. PR [#1595](https://github.com/fastapi/sqlmodel/pull/1595) by [@spazm](https://github.com/spazm). + ### Docs * 📝 Fix typo in `docs/tutorial/fastapi/simple-hero-api.md`. PR [#1583](https://github.com/fastapi/sqlmodel/pull/1583) by [@kofi-kusi](https://github.com/kofi-kusi). From 35a652e059db362b082303551bfdae4f1eab2e61 Mon Sep 17 00:00:00 2001 From: Sofie Van Landeghem Date: Wed, 8 Oct 2025 12:53:29 +0200 Subject: [PATCH 769/906] =?UTF-8?q?=E2=9C=85=20Add=20test=20that=20runs=20?= =?UTF-8?q?select=20with=203=20or=204=20arguments=20(#1590)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- scripts/lint.sh | 1 + tests/test_select_typing.py | 64 +++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 tests/test_select_typing.py diff --git a/scripts/lint.sh b/scripts/lint.sh index 7fab52df57..e4a7b5bea7 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -4,5 +4,6 @@ set -e set -x mypy sqlmodel +mypy tests/test_select_typing.py ruff check sqlmodel tests docs_src scripts ruff format sqlmodel tests docs_src scripts --check diff --git a/tests/test_select_typing.py b/tests/test_select_typing.py new file mode 100644 index 0000000000..8f1d030631 --- /dev/null +++ b/tests/test_select_typing.py @@ -0,0 +1,64 @@ +from typing import Optional + +from sqlmodel import Field, Session, SQLModel, create_engine, select +from sqlmodel.pool import StaticPool + + +def test_fields() -> None: + class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str + secret_name: str + age: Optional[int] = None + food: Optional[str] = None + + engine = create_engine( + "sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool + ) + + SQLModel.metadata.create_all(engine) + + with Session(engine) as session: + session.add(Hero(name="Deadpond", secret_name="Dive Wilson")) + session.add( + Hero(name="Spider-Boy", secret_name="Pedro Parqueador", food="pizza") + ) + session.add(Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)) + + session.commit() + + # check typing of select with 3 fields + with Session(engine) as session: + statement_3 = select(Hero.id, Hero.name, Hero.secret_name) + results_3 = session.exec(statement_3) + for hero_3 in results_3: + assert len(hero_3) == 3 + name_3: str = hero_3[1] + assert type(name_3) is str + assert type(hero_3[0]) is int + assert type(hero_3[2]) is str + + # check typing of select with 4 fields + with Session(engine) as session: + statement_4 = select(Hero.id, Hero.name, Hero.secret_name, Hero.age) + results_4 = session.exec(statement_4) + for hero_4 in results_4: + assert len(hero_4) == 4 + name_4: str = hero_4[1] + assert type(name_4) is str + assert type(hero_4[0]) is int + assert type(hero_4[2]) is str + assert type(hero_4[3]) in [int, type(None)] + + # check typing of select with 5 fields: currently runs but doesn't pass mypy + # with Session(engine) as session: + # statement_5 = select(Hero.id, Hero.name, Hero.secret_name, Hero.age, Hero.food) + # results_5 = session.exec(statement_5) + # for hero_5 in results_5: + # assert len(hero_5) == 5 + # name_5: str = hero_5[1] + # assert type(name_5) is str + # assert type(hero_5[0]) is int + # assert type(hero_5[2]) is str + # assert type(hero_5[3]) in [int, type(None)] + # assert type(hero_5[4]) in [str, type(None)] From 2d3b25dd24fb09bb5e02fdd444b5da4ad2adcbbd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 8 Oct 2025 10:53:49 +0000 Subject: [PATCH 770/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 42e193641b..721b795191 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -12,6 +12,7 @@ ### Internal +* ✅ Add test that runs select with 3 or 4 arguments. PR [#1590](https://github.com/fastapi/sqlmodel/pull/1590) by [@svlandeg](https://github.com/svlandeg). * ⬆ Bump mkdocs-macros-plugin from 1.3.9 to 1.4.0. PR [#1581](https://github.com/fastapi/sqlmodel/pull/1581) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-material from 9.6.20 to 9.6.21. PR [#1588](https://github.com/fastapi/sqlmodel/pull/1588) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1584](https://github.com/fastapi/sqlmodel/pull/1584) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From f751e1c7976f08ed3674b890aa7e28a74d47c6e6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Oct 2025 13:10:30 +0200 Subject: [PATCH 771/906] =?UTF-8?q?=E2=AC=86=20Bump=20mypy=20from=201.4.1?= =?UTF-8?q?=20to=201.18.2=20(#1560)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ⬆ Bump mypy from 1.4.1 to 1.18.1 Bumps [mypy](https://github.com/python/mypy) from 1.4.1 to 1.18.1. - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.4.1...v1.18.1) --- updated-dependencies: - dependency-name: mypy dependency-version: 1.18.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * restrict to latest version that supports python 3.8 * remove unnecssary ignore statement * add ignore statement * make ignore statement more specific * expand on mypy command to debug CI failure * experiment with from future import * use the latest mypy for Python 3.9 and up * fix type of keys to be removed * annotate origin as Any to avoid type issues * bump to 1.10.0 only for now * exclude one particular file from mypy processing * Try to bump to 1.18.2 again * attempt to remove the future import again * add back future import --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sofie Van Landeghem Co-authored-by: svlandeg --- pyproject.toml | 5 +---- requirements-tests.txt | 4 +++- sqlmodel/_compat.py | 6 +++--- sqlmodel/main.py | 4 +++- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4ae195ac73..902fffdc9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -98,10 +98,7 @@ show_contexts = true [tool.mypy] strict = true - -[[tool.mypy.overrides]] -module = "sqlmodel.sql._expression_select_gen" -warn_unused_ignores = false +exclude = "sqlmodel.sql._expression_select_gen" [[tool.mypy.overrides]] module = "docs_src.*" diff --git a/requirements-tests.txt b/requirements-tests.txt index f6e826511d..6cae1015c9 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -2,7 +2,9 @@ -r requirements-docs-tests.txt pytest >=7.0.1,<9.0.0 coverage[toml] >=6.2,<8.0 -mypy ==1.4.1 +# Remove when support for Python 3.8 is dropped +mypy ==1.14.1; python_version < "3.9" +mypy ==1.18.2; python_version >= "3.9" ruff ==0.13.2 # For FastAPI tests fastapi >=0.103.2 diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index 38dd501c4a..dc806d381b 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -123,7 +123,7 @@ def init_pydantic_private_attrs(new_object: InstanceOrType["SQLModel"]) -> None: object.__setattr__(new_object, "__pydantic_private__", None) def get_annotations(class_dict: Dict[str, Any]) -> Dict[str, Any]: - return class_dict.get("__annotations__", {}) + return class_dict.get("__annotations__", {}) # type: ignore[no-any-return] def is_table_model_class(cls: Type[Any]) -> bool: config = getattr(cls, "model_config", {}) @@ -180,7 +180,7 @@ def is_field_noneable(field: "FieldInfo") -> bool: if not field.is_required(): if field.default is Undefined: return False - if field.annotation is None or field.annotation is NoneType: # type: ignore[comparison-overlap] + if field.annotation is None or field.annotation is NoneType: return True return False return False @@ -509,7 +509,7 @@ def _calculate_keys( keys -= update.keys() if exclude: - keys -= {k for k, v in exclude.items() if ValueItems.is_true(v)} + keys -= {str(k) for k, v in exclude.items() if ValueItems.is_true(v)} return keys diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 1925a48f04..7c916f79af 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import ipaddress import uuid import weakref @@ -601,7 +603,7 @@ def __init__( setattr(cls, rel_name, rel_info.sa_relationship) # Fix #315 continue raw_ann = cls.__annotations__[rel_name] - origin = get_origin(raw_ann) + origin: Any = get_origin(raw_ann) if origin is Mapped: ann = raw_ann.__args__[0] else: From 5644ab4a29c7a4ec681be95b5e98c0231fa25d8e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 8 Oct 2025 11:10:52 +0000 Subject: [PATCH 772/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 721b795191..a264524f56 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -12,6 +12,7 @@ ### Internal +* ⬆ Bump mypy from 1.4.1 to 1.18.2. PR [#1560](https://github.com/fastapi/sqlmodel/pull/1560) by [@dependabot[bot]](https://github.com/apps/dependabot). * ✅ Add test that runs select with 3 or 4 arguments. PR [#1590](https://github.com/fastapi/sqlmodel/pull/1590) by [@svlandeg](https://github.com/svlandeg). * ⬆ Bump mkdocs-macros-plugin from 1.3.9 to 1.4.0. PR [#1581](https://github.com/fastapi/sqlmodel/pull/1581) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-material from 9.6.20 to 9.6.21. PR [#1588](https://github.com/fastapi/sqlmodel/pull/1588) by [@dependabot[bot]](https://github.com/apps/dependabot). From 0cbf2e6049cb2cfea8c8754f39b7617ab505b72c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 8 Oct 2025 13:17:26 +0200 Subject: [PATCH 773/906] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.0.?= =?UTF-8?q?26?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 2 ++ sqlmodel/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index a264524f56..1917ea0473 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest Changes +## 0.0.26 + ### Fixes * 🐛 Fix attribute handling in `model_dump` for compatibility with the latest Pydantic versions. PR [#1595](https://github.com/fastapi/sqlmodel/pull/1595) by [@spazm](https://github.com/spazm). diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index aec97b7eb9..ee0d40f7b8 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.0.25" +__version__ = "0.0.26" # Re-export from SQLAlchemy from sqlalchemy.engine import create_engine as create_engine From 45215fcecad3ce2ed0aac40c44818d256e5b19ec Mon Sep 17 00:00:00 2001 From: Sofie Van Landeghem Date: Wed, 8 Oct 2025 18:26:33 +0200 Subject: [PATCH 774/906] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Add=20support=20fo?= =?UTF-8?q?r=20Python=203.14=20(#1578)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 8 +++++--- pyproject.toml | 1 + sqlmodel/_compat.py | 16 +++++++++++++++- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c4fdb911a8..fb01245f45 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,9 +26,8 @@ jobs: strategy: matrix: os: [ ubuntu-latest, windows-latest, macos-latest ] - python-version: [ "3.13" ] + python-version: [ "3.14" ] pydantic-version: - - pydantic-v1 - pydantic-v2 include: - os: macos-latest @@ -47,7 +46,10 @@ jobs: python-version: "3.12" pydantic-version: pydantic-v1 - os: ubuntu-latest - python-version: "3.12" + python-version: "3.13" + pydantic-version: pydantic-v1 + - os: macos-latest + python-version: "3.13" pydantic-version: pydantic-v2 fail-fast: false runs-on: ${{ matrix.os }} diff --git a/pyproject.toml b/pyproject.toml index 902fffdc9f..cd47f5ecd9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Database", "Topic :: Database :: Database Engines/Servers", "Topic :: Internet", diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index dc806d381b..230f8cc362 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -1,3 +1,4 @@ +import sys import types from contextlib import contextmanager from contextvars import ContextVar @@ -123,7 +124,20 @@ def init_pydantic_private_attrs(new_object: InstanceOrType["SQLModel"]) -> None: object.__setattr__(new_object, "__pydantic_private__", None) def get_annotations(class_dict: Dict[str, Any]) -> Dict[str, Any]: - return class_dict.get("__annotations__", {}) # type: ignore[no-any-return] + raw_annotations: Dict[str, Any] = class_dict.get("__annotations__", {}) + if sys.version_info >= (3, 14) and "__annotations__" not in class_dict: + # See https://github.com/pydantic/pydantic/pull/11991 + from annotationlib import ( + Format, + call_annotate_function, + get_annotate_from_class_namespace, + ) + + if annotate := get_annotate_from_class_namespace(class_dict): + raw_annotations = call_annotate_function( + annotate, format=Format.FORWARDREF + ) + return raw_annotations def is_table_model_class(cls: Type[Any]) -> bool: config = getattr(cls, "model_config", {}) From f9f4faf68bcd3670b98983cccc0b139412b744a8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 8 Oct 2025 16:26:52 +0000 Subject: [PATCH 775/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 1917ea0473..81044d7c85 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Upgrades + +* ⬆️ Add support for Python 3.14. PR [#1578](https://github.com/fastapi/sqlmodel/pull/1578) by [@svlandeg](https://github.com/svlandeg). + ## 0.0.26 ### Fixes From a85de910d1706206a7016a277db698d10abf825d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 8 Oct 2025 18:37:37 +0200 Subject: [PATCH 776/906] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.0.?= =?UTF-8?q?27?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 2 ++ sqlmodel/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 81044d7c85..c35e5cbe02 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest Changes +## 0.0.27 + ### Upgrades * ⬆️ Add support for Python 3.14. PR [#1578](https://github.com/fastapi/sqlmodel/pull/1578) by [@svlandeg](https://github.com/svlandeg). diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index ee0d40f7b8..9175458980 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.0.26" +__version__ = "0.0.27" # Re-export from SQLAlchemy from sqlalchemy.engine import create_engine as create_engine From 95c6b2d673d84058ba554123c7a9254aed639fd2 Mon Sep 17 00:00:00 2001 From: Sofie Van Landeghem Date: Tue, 21 Oct 2025 11:29:39 +0200 Subject: [PATCH 777/906] =?UTF-8?q?=E2=9C=85=20Remove=20unused=20type=20ig?= =?UTF-8?q?nores=20since=20SQLAlchemy=202.0.44=20(#1613)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/ext/asyncio/session.py | 2 +- sqlmodel/orm/session.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sqlmodel/ext/asyncio/session.py b/sqlmodel/ext/asyncio/session.py index 54488357bb..ff99dff899 100644 --- a/sqlmodel/ext/asyncio/session.py +++ b/sqlmodel/ext/asyncio/session.py @@ -129,7 +129,7 @@ async def exec( ``` """ ) - async def execute( # type: ignore + async def execute( self, statement: _Executable, params: Optional[_CoreAnyExecuteParams] = None, diff --git a/sqlmodel/orm/session.py b/sqlmodel/orm/session.py index dca4733d61..9e82d48a73 100644 --- a/sqlmodel/orm/session.py +++ b/sqlmodel/orm/session.py @@ -113,7 +113,7 @@ def exec( """, category=None, ) - def execute( # type: ignore + def execute( self, statement: _Executable, params: Optional[_CoreAnyExecuteParams] = None, From 2d5617f1dafd2499a09d7de9b19dd6849fd1c65c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 21 Oct 2025 09:30:07 +0000 Subject: [PATCH 778/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index c35e5cbe02..43bf5536c1 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Internal + +* ✅ Remove unused type ignores since SQLAlchemy 2.0.44. PR [#1613](https://github.com/fastapi/sqlmodel/pull/1613) by [@svlandeg](https://github.com/svlandeg). + ## 0.0.27 ### Upgrades From 326dc13ac3596dceb44e981cd71295cb15656c5e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 12:45:13 +0200 Subject: [PATCH 779/906] =?UTF-8?q?=E2=AC=86=20Bump=20typer=20from=200.19.?= =?UTF-8?q?2=20to=200.20.0=20(#1612)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [typer](https://github.com/fastapi/typer) from 0.19.2 to 0.20.0. - [Release notes](https://github.com/fastapi/typer/releases) - [Changelog](https://github.com/fastapi/typer/blob/master/docs/release-notes.md) - [Commits](https://github.com/fastapi/typer/compare/0.19.2...0.20.0) --- updated-dependencies: - dependency-name: typer dependency-version: 0.20.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sofie Van Landeghem --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index b6479d582c..bbac47e811 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -13,6 +13,6 @@ cairosvg==2.8.2 # mkdocstrings[python]==0.25.1 griffe-typingdoc==0.2.9 # For griffe, it formats with black -typer == 0.19.2 +typer == 0.20.0 mkdocs-macros-plugin==1.4.0 markdown-include-variants==0.0.5 From c14e926ce622bc0e005a76576192fb0f02bdf89d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 21 Oct 2025 10:45:38 +0000 Subject: [PATCH 780/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 43bf5536c1..fc38c45888 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Internal +* ⬆ Bump typer from 0.19.2 to 0.20.0. PR [#1612](https://github.com/fastapi/sqlmodel/pull/1612) by [@dependabot[bot]](https://github.com/apps/dependabot). * ✅ Remove unused type ignores since SQLAlchemy 2.0.44. PR [#1613](https://github.com/fastapi/sqlmodel/pull/1613) by [@svlandeg](https://github.com/svlandeg). ## 0.0.27 From 5d2ba6fab7e9e24ee9a665134c93f18003b8b617 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Tue, 21 Oct 2025 12:45:57 +0200 Subject: [PATCH 781/906] =?UTF-8?q?=F0=9F=94=A7=20Configure=20reminder=20f?= =?UTF-8?q?or=20`waiting`=20label=20in=20`issue-manager`=20(#1609)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Configure reminder for waiting lable in issue manager config Co-authored-by: Sofie Van Landeghem --- .github/workflows/issue-manager.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml index 90d3d5fd4f..137ed16de3 100644 --- a/.github/workflows/issue-manager.yml +++ b/.github/workflows/issue-manager.yml @@ -38,7 +38,11 @@ jobs: }, "waiting": { "delay": 2628000, - "message": "As this PR has been waiting for the original user for a while but seems to be inactive, it's now going to be closed. But if there's anyone interested, feel free to create a new PR." + "message": "As this PR has been waiting for the original user for a while but seems to be inactive, it's now going to be closed. But if there's anyone interested, feel free to create a new PR.", + "reminder": { + "before": "P3D", + "message": "Heads-up: this will be closed in 3 days unless there’s new activity." + } }, "invalid": { "delay": 0, From 3d5a5177cd50ef6b534d13bf37dc08103824d09f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 12:46:22 +0200 Subject: [PATCH 782/906] =?UTF-8?q?=E2=AC=86=20Bump=20mkdocs-material=20fr?= =?UTF-8?q?om=209.6.21=20to=209.6.22=20(#1608)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.6.21 to 9.6.22. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.6.21...9.6.22) --- updated-dependencies: - dependency-name: mkdocs-material dependency-version: 9.6.22 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sofie Van Landeghem --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index bbac47e811..d0c0b1a728 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,6 +1,6 @@ -e . -r requirements-docs-tests.txt -mkdocs-material==9.6.21 +mkdocs-material==9.6.22 mdx-include >=1.4.1,<2.0.0 mkdocs-redirects>=1.2.1,<1.3.0 pyyaml >=5.3.1,<7.0.0 From 4bdbb763e4b333610ae48824b80953c38a37c414 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 21 Oct 2025 10:46:31 +0000 Subject: [PATCH 783/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index fc38c45888..db9749c195 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Internal +* 🔧 Configure reminder for `waiting` label in `issue-manager`. PR [#1609](https://github.com/fastapi/sqlmodel/pull/1609) by [@YuriiMotov](https://github.com/YuriiMotov). * ⬆ Bump typer from 0.19.2 to 0.20.0. PR [#1612](https://github.com/fastapi/sqlmodel/pull/1612) by [@dependabot[bot]](https://github.com/apps/dependabot). * ✅ Remove unused type ignores since SQLAlchemy 2.0.44. PR [#1613](https://github.com/fastapi/sqlmodel/pull/1613) by [@svlandeg](https://github.com/svlandeg). From d0133a34aabaea51bde1e7029ced453fafaee086 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 21 Oct 2025 10:46:55 +0000 Subject: [PATCH 784/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index db9749c195..d375a615df 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Internal +* ⬆ Bump mkdocs-material from 9.6.21 to 9.6.22. PR [#1608](https://github.com/fastapi/sqlmodel/pull/1608) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔧 Configure reminder for `waiting` label in `issue-manager`. PR [#1609](https://github.com/fastapi/sqlmodel/pull/1609) by [@YuriiMotov](https://github.com/YuriiMotov). * ⬆ Bump typer from 0.19.2 to 0.20.0. PR [#1612](https://github.com/fastapi/sqlmodel/pull/1612) by [@dependabot[bot]](https://github.com/apps/dependabot). * ✅ Remove unused type ignores since SQLAlchemy 2.0.44. PR [#1613](https://github.com/fastapi/sqlmodel/pull/1613) by [@svlandeg](https://github.com/svlandeg). From 68a39c75aceee873a20ed0376ba6ff0047d56cb7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 12:47:51 +0200 Subject: [PATCH 785/906] =?UTF-8?q?=E2=AC=86=20Bump=20astral-sh/setup-uv?= =?UTF-8?q?=20from=206=20to=207=20(#1593)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 6 to 7. - [Release notes](https://github.com/astral-sh/setup-uv/releases) - [Commits](https://github.com/astral-sh/setup-uv/compare/v6...v7) --- updated-dependencies: - dependency-name: astral-sh/setup-uv dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sofie Van Landeghem --- .github/workflows/build-docs.yml | 2 +- .github/workflows/deploy-docs.yml | 2 +- .github/workflows/smokeshow.yml | 2 +- .github/workflows/test.yml | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 7426a744b3..0e44035915 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -59,7 +59,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index c6f57dbb64..0704bbcb30 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -29,7 +29,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index 7861b0e27c..b08add222f 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -21,7 +21,7 @@ jobs: with: python-version: '3.9' - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fb01245f45..60e3329623 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -60,7 +60,7 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true @@ -107,7 +107,7 @@ jobs: with: python-version: '3.13' - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 with: version: "0.4.15" enable-cache: true From 8e2fce731abae9bd95bd8ec150952d9b0d670fcc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 21 Oct 2025 10:48:37 +0000 Subject: [PATCH 786/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index d375a615df..396dc010bf 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Internal +* ⬆ Bump astral-sh/setup-uv from 6 to 7. PR [#1593](https://github.com/fastapi/sqlmodel/pull/1593) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-material from 9.6.21 to 9.6.22. PR [#1608](https://github.com/fastapi/sqlmodel/pull/1608) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔧 Configure reminder for `waiting` label in `issue-manager`. PR [#1609](https://github.com/fastapi/sqlmodel/pull/1609) by [@YuriiMotov](https://github.com/YuriiMotov). * ⬆ Bump typer from 0.19.2 to 0.20.0. PR [#1612](https://github.com/fastapi/sqlmodel/pull/1612) by [@dependabot[bot]](https://github.com/apps/dependabot). From 83dcd587b094c77e37a438723cdbe2eac01e4146 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 12:48:54 +0200 Subject: [PATCH 787/906] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#1605)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.13.3 → v0.14.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.13.3...v0.14.1) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sofie Van Landeghem --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9c075f68ea..34f2120194 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.13.3 + rev: v0.14.1 hooks: - id: ruff args: From acba5e707bb1ca90b4f9bcb55e6a32a51fdbcd88 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 12:48:58 +0200 Subject: [PATCH 788/906] =?UTF-8?q?=E2=AC=86=20Bump=20ruff=20from=200.13.2?= =?UTF-8?q?=20to=200.14.0=20(#1592)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ruff](https://github.com/astral-sh/ruff) from 0.13.2 to 0.14.0. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.13.2...0.14.0) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.14.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sofie Van Landeghem Co-authored-by: svlandeg --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 6cae1015c9..ccdc54ff38 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -5,7 +5,7 @@ coverage[toml] >=6.2,<8.0 # Remove when support for Python 3.8 is dropped mypy ==1.14.1; python_version < "3.9" mypy ==1.18.2; python_version >= "3.9" -ruff ==0.13.2 +ruff ==0.14.0 # For FastAPI tests fastapi >=0.103.2 httpx ==0.28.1 From f1a42c44a83fe58d33e658bc15345b225843f433 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 21 Oct 2025 10:49:40 +0000 Subject: [PATCH 789/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 396dc010bf..d86f4868b2 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1605](https://github.com/fastapi/sqlmodel/pull/1605) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump astral-sh/setup-uv from 6 to 7. PR [#1593](https://github.com/fastapi/sqlmodel/pull/1593) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-material from 9.6.21 to 9.6.22. PR [#1608](https://github.com/fastapi/sqlmodel/pull/1608) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔧 Configure reminder for `waiting` label in `issue-manager`. PR [#1609](https://github.com/fastapi/sqlmodel/pull/1609) by [@YuriiMotov](https://github.com/YuriiMotov). From 36cf22f2e026d1aee43edf083783b4776b60eed0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 21 Oct 2025 10:50:14 +0000 Subject: [PATCH 790/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index d86f4868b2..d5f506f80a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Internal +* ⬆ Bump ruff from 0.13.2 to 0.14.0. PR [#1592](https://github.com/fastapi/sqlmodel/pull/1592) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1605](https://github.com/fastapi/sqlmodel/pull/1605) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump astral-sh/setup-uv from 6 to 7. PR [#1593](https://github.com/fastapi/sqlmodel/pull/1593) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-material from 9.6.21 to 9.6.22. PR [#1608](https://github.com/fastapi/sqlmodel/pull/1608) by [@dependabot[bot]](https://github.com/apps/dependabot). From acdd6c86fb9368f4b768dcefcee5614cf2e1ccbf Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Tue, 21 Oct 2025 14:53:05 +0200 Subject: [PATCH 791/906] =?UTF-8?q?=F0=9F=93=9D=20Fix=20broken=20links=20i?= =?UTF-8?q?n=20docs=20(#1601)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix broken links in docs --- docs/tutorial/create-db-and-table-with-db-browser.md | 2 +- docs/tutorial/create-db-and-table.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/tutorial/create-db-and-table-with-db-browser.md b/docs/tutorial/create-db-and-table-with-db-browser.md index b3a6f960ae..4c535df11c 100644 --- a/docs/tutorial/create-db-and-table-with-db-browser.md +++ b/docs/tutorial/create-db-and-table-with-db-browser.md @@ -40,7 +40,7 @@ Click the button New Database. -A dialog should show up. Go to the [project directory you created](./index.md#create-a-project){.internal-link target=_blank} and save the file with a name of `database.db`. +A dialog should show up. Go to the [project directory you created](../virtual-environments.md#create-a-project){.internal-link target=_blank} and save the file with a name of `database.db`. /// tip diff --git a/docs/tutorial/create-db-and-table.md b/docs/tutorial/create-db-and-table.md index 2f2f34c828..688567ed4d 100644 --- a/docs/tutorial/create-db-and-table.md +++ b/docs/tutorial/create-db-and-table.md @@ -2,7 +2,7 @@ Now let's get to the code. 👩‍💻 -Make sure you are inside of your project directory and with your virtual environment activated as [explained in the previous chapter](index.md){.internal-link target=_blank}. +Make sure you are inside of your project directory and with your virtual environment activated as explained in [Virtual Environments](../virtual-environments.md#create-a-project){.internal-link target=_blank}. We will: @@ -327,7 +327,7 @@ Put the code it in a file `app.py` if you haven't already. /// tip -Remember to [activate the virtual environment](./index.md#create-a-python-virtual-environment){.internal-link target=_blank} before running it. +Remember to [activate the virtual environment](../virtual-environments.md#create-a-virtual-environment){.internal-link target=_blank} before running it. /// From 49a53c24409a30e88dfc5a840dba84d1a950f2ae Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 21 Oct 2025 12:53:27 +0000 Subject: [PATCH 792/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index d5f506f80a..8cd752b443 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Docs + +* 📝 Fix broken links in docs. PR [#1601](https://github.com/fastapi/sqlmodel/pull/1601) by [@YuriiMotov](https://github.com/YuriiMotov). + ### Internal * ⬆ Bump ruff from 0.13.2 to 0.14.0. PR [#1592](https://github.com/fastapi/sqlmodel/pull/1592) by [@dependabot[bot]](https://github.com/apps/dependabot). From b99abe810e52274cb31504035860bca9a0b099fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Oct 2025 13:09:11 +0200 Subject: [PATCH 793/906] =?UTF-8?q?=E2=AC=86=20Bump=20ruff=20from=200.14.0?= =?UTF-8?q?=20to=200.14.1=20(#1614)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.0 to 0.14.1. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.14.0...0.14.1) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.14.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index ccdc54ff38..70dc2a9a09 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -5,7 +5,7 @@ coverage[toml] >=6.2,<8.0 # Remove when support for Python 3.8 is dropped mypy ==1.14.1; python_version < "3.9" mypy ==1.18.2; python_version >= "3.9" -ruff ==0.14.0 +ruff ==0.14.1 # For FastAPI tests fastapi >=0.103.2 httpx ==0.28.1 From c60cf8608870977cd71f61927c1254756fd02239 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 22 Oct 2025 11:09:44 +0000 Subject: [PATCH 794/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 8cd752b443..4f4be12503 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* ⬆ Bump ruff from 0.14.0 to 0.14.1. PR [#1614](https://github.com/fastapi/sqlmodel/pull/1614) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.13.2 to 0.14.0. PR [#1592](https://github.com/fastapi/sqlmodel/pull/1592) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1605](https://github.com/fastapi/sqlmodel/pull/1605) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump astral-sh/setup-uv from 6 to 7. PR [#1593](https://github.com/fastapi/sqlmodel/pull/1593) by [@dependabot[bot]](https://github.com/apps/dependabot). From cb13d12064a9738dc92394e7b0e6378e6711611e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 11:40:56 +0100 Subject: [PATCH 795/906] =?UTF-8?q?=E2=AC=86=20Bump=20actions/download-art?= =?UTF-8?q?ifact=20from=205=20to=206=20(#1621)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 5 to 6. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/deploy-docs.yml | 2 +- .github/workflows/smokeshow.yml | 2 +- .github/workflows/test.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 0704bbcb30..05e5a0685c 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -49,7 +49,7 @@ jobs: run: | rm -rf ./site mkdir ./site - - uses: actions/download-artifact@v5 + - uses: actions/download-artifact@v6 with: path: ./site/ pattern: docs-site diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index b08add222f..79d1ded7ee 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -29,7 +29,7 @@ jobs: requirements**.txt pyproject.toml - run: uv pip install -r requirements-github-actions.txt - - uses: actions/download-artifact@v5 + - uses: actions/download-artifact@v6 with: name: coverage-html path: htmlcov diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 60e3329623..c296a07d3e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -115,7 +115,7 @@ jobs: requirements**.txt pyproject.toml - name: Get coverage files - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 with: pattern: coverage-* path: coverage From 2d4bc636b41f4d05d6b0c34fb6cb1b2ac70f3b4b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 11:41:14 +0100 Subject: [PATCH 796/906] =?UTF-8?q?=E2=AC=86=20Bump=20actions/upload-artif?= =?UTF-8?q?act=20from=204=20to=205=20(#1620)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-docs.yml | 2 +- .github/workflows/test.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 0e44035915..d4d4737e99 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -81,7 +81,7 @@ jobs: run: python ./scripts/docs.py verify-readme - name: Build Docs run: python ./scripts/docs.py build - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v5 with: name: docs-site path: ./site/** diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c296a07d3e..8392d92438 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -91,7 +91,7 @@ jobs: COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}-${{ matrix.pydantic-version }} CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }} - name: Store coverage files - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: coverage-${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.pydantic-version }} path: coverage @@ -127,7 +127,7 @@ jobs: - run: coverage report - run: coverage html --title "Coverage for ${{ github.sha }}" - name: Store coverage HTML - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: coverage-html path: htmlcov From 4abebca3ed3c4a436a9f4faa07f447934a69379f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 27 Oct 2025 10:41:24 +0000 Subject: [PATCH 797/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 4f4be12503..71ca78d89d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* ⬆ Bump actions/download-artifact from 5 to 6. PR [#1621](https://github.com/fastapi/sqlmodel/pull/1621) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.14.0 to 0.14.1. PR [#1614](https://github.com/fastapi/sqlmodel/pull/1614) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.13.2 to 0.14.0. PR [#1592](https://github.com/fastapi/sqlmodel/pull/1592) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1605](https://github.com/fastapi/sqlmodel/pull/1605) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From 8f685ab5f8997b63aba4817b77d89a210fbe70b7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 27 Oct 2025 10:41:49 +0000 Subject: [PATCH 798/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 71ca78d89d..8433df5225 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* ⬆ Bump actions/upload-artifact from 4 to 5. PR [#1620](https://github.com/fastapi/sqlmodel/pull/1620) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/download-artifact from 5 to 6. PR [#1621](https://github.com/fastapi/sqlmodel/pull/1621) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.14.0 to 0.14.1. PR [#1614](https://github.com/fastapi/sqlmodel/pull/1614) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.13.2 to 0.14.0. PR [#1592](https://github.com/fastapi/sqlmodel/pull/1592) by [@dependabot[bot]](https://github.com/apps/dependabot). From 4f1684d9eca89747e82a3e3b009ddc869dbaf86d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 11:44:55 +0100 Subject: [PATCH 799/906] =?UTF-8?q?=E2=AC=86=20Bump=20griffe-typingdoc=20f?= =?UTF-8?q?rom=200.2.9=20to=200.3.0=20(#1615)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [griffe-typingdoc](https://github.com/mkdocstrings/griffe-typingdoc) from 0.2.9 to 0.3.0. - [Release notes](https://github.com/mkdocstrings/griffe-typingdoc/releases) - [Changelog](https://github.com/mkdocstrings/griffe-typingdoc/blob/main/CHANGELOG.md) - [Commits](https://github.com/mkdocstrings/griffe-typingdoc/compare/0.2.9...0.3.0) --- updated-dependencies: - dependency-name: griffe-typingdoc dependency-version: 0.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index d0c0b1a728..338c703d6b 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -11,7 +11,7 @@ pillow==11.3.0 # For image processing by Material for MkDocs cairosvg==2.8.2 # mkdocstrings[python]==0.25.1 -griffe-typingdoc==0.2.9 +griffe-typingdoc==0.3.0 # For griffe, it formats with black typer == 0.20.0 mkdocs-macros-plugin==1.4.0 From c589c92885919fe81da62451791aef93ed56ac51 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 27 Oct 2025 10:45:18 +0000 Subject: [PATCH 800/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 8433df5225..00e7e29e26 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* ⬆ Bump griffe-typingdoc from 0.2.9 to 0.3.0. PR [#1615](https://github.com/fastapi/sqlmodel/pull/1615) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/upload-artifact from 4 to 5. PR [#1620](https://github.com/fastapi/sqlmodel/pull/1620) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/download-artifact from 5 to 6. PR [#1621](https://github.com/fastapi/sqlmodel/pull/1621) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.14.0 to 0.14.1. PR [#1614](https://github.com/fastapi/sqlmodel/pull/1614) by [@dependabot[bot]](https://github.com/apps/dependabot). From d97a1cfc0d9496c4cbfb83c23583f6c2c544da76 Mon Sep 17 00:00:00 2001 From: Sofie Van Landeghem Date: Mon, 27 Oct 2025 15:44:34 +0100 Subject: [PATCH 801/906] =?UTF-8?q?=F0=9F=94=A7=20Add=20PEP-639=20license?= =?UTF-8?q?=20metadata=20(#1624)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cd47f5ecd9..048f38844c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,8 @@ requires-python = ">=3.8" authors = [ { name = "Sebastián Ramírez", email = "tiangolo@gmail.com" }, ] +license = "MIT" +license-files = ["LICENSE"] classifiers = [ "Development Status :: 4 - Beta", @@ -18,7 +20,6 @@ classifiers = [ "Intended Audience :: Developers", "Intended Audience :: Science/Research", "Intended Audience :: System Administrators", - "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", From 3f359c3c14af9b578ba44890d1a7aae487cd319b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 27 Oct 2025 14:47:04 +0000 Subject: [PATCH 802/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 00e7e29e26..0a1873a7b7 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* 🔧 Add PEP-639 license metadata. PR [#1624](https://github.com/fastapi/sqlmodel/pull/1624) by [@svlandeg](https://github.com/svlandeg). * ⬆ Bump griffe-typingdoc from 0.2.9 to 0.3.0. PR [#1615](https://github.com/fastapi/sqlmodel/pull/1615) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/upload-artifact from 4 to 5. PR [#1620](https://github.com/fastapi/sqlmodel/pull/1620) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/download-artifact from 5 to 6. PR [#1621](https://github.com/fastapi/sqlmodel/pull/1621) by [@dependabot[bot]](https://github.com/apps/dependabot). From e0102d191b408aa4fd12008fd6d3af825ea8d369 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 08:50:32 +0100 Subject: [PATCH 803/906] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#1625)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.1 → v0.14.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.1...v0.14.2) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 34f2120194..25dcd7b885 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.1 + rev: v0.14.2 hooks: - id: ruff args: From 7ee1ce052c4e174ae169a5d4c6c6462e2654da1e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 08:50:41 +0100 Subject: [PATCH 804/906] =?UTF-8?q?=E2=AC=86=20Bump=20ruff=20from=200.14.1?= =?UTF-8?q?=20to=200.14.2=20(#1616)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.1 to 0.14.2. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.14.1...0.14.2) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.14.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 70dc2a9a09..f301755303 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -5,7 +5,7 @@ coverage[toml] >=6.2,<8.0 # Remove when support for Python 3.8 is dropped mypy ==1.14.1; python_version < "3.9" mypy ==1.18.2; python_version >= "3.9" -ruff ==0.14.1 +ruff ==0.14.2 # For FastAPI tests fastapi >=0.103.2 httpx ==0.28.1 From a471ea95bc62bafce664137b57977da7db496e0b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 08:50:53 +0100 Subject: [PATCH 805/906] =?UTF-8?q?=E2=AC=86=20Bump=20mkdocs-macros-plugin?= =?UTF-8?q?=20from=201.4.0=20to=201.4.1=20(#1626)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mkdocs-macros-plugin](https://github.com/fralau/mkdocs_macros_plugin) from 1.4.0 to 1.4.1. - [Release notes](https://github.com/fralau/mkdocs_macros_plugin/releases) - [Changelog](https://github.com/fralau/mkdocs-macros-plugin/blob/master/CHANGELOG.md) - [Commits](https://github.com/fralau/mkdocs_macros_plugin/compare/v1.4.0...v1.4.1) --- updated-dependencies: - dependency-name: mkdocs-macros-plugin dependency-version: 1.4.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 338c703d6b..fff91420ff 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -14,5 +14,5 @@ cairosvg==2.8.2 griffe-typingdoc==0.3.0 # For griffe, it formats with black typer == 0.20.0 -mkdocs-macros-plugin==1.4.0 +mkdocs-macros-plugin==1.4.1 markdown-include-variants==0.0.5 From 0bbb9f0c6d210ad6e17b7251fc7c2908b4b6668c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 28 Oct 2025 07:52:16 +0000 Subject: [PATCH 806/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 0a1873a7b7..747dd9f389 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1625](https://github.com/fastapi/sqlmodel/pull/1625) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * 🔧 Add PEP-639 license metadata. PR [#1624](https://github.com/fastapi/sqlmodel/pull/1624) by [@svlandeg](https://github.com/svlandeg). * ⬆ Bump griffe-typingdoc from 0.2.9 to 0.3.0. PR [#1615](https://github.com/fastapi/sqlmodel/pull/1615) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/upload-artifact from 4 to 5. PR [#1620](https://github.com/fastapi/sqlmodel/pull/1620) by [@dependabot[bot]](https://github.com/apps/dependabot). From 893ebe0c2e802a3e4936bc8cb42172746424b5b3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 28 Oct 2025 07:52:25 +0000 Subject: [PATCH 807/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 747dd9f389..565068ff2d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* ⬆ Bump ruff from 0.14.1 to 0.14.2. PR [#1616](https://github.com/fastapi/sqlmodel/pull/1616) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1625](https://github.com/fastapi/sqlmodel/pull/1625) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * 🔧 Add PEP-639 license metadata. PR [#1624](https://github.com/fastapi/sqlmodel/pull/1624) by [@svlandeg](https://github.com/svlandeg). * ⬆ Bump griffe-typingdoc from 0.2.9 to 0.3.0. PR [#1615](https://github.com/fastapi/sqlmodel/pull/1615) by [@dependabot[bot]](https://github.com/apps/dependabot). From 9039c2979b4d0c7c67ef717b7aa562a01d700e11 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 28 Oct 2025 07:53:06 +0000 Subject: [PATCH 808/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 565068ff2d..8540bb7b39 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ ### Internal +* ⬆ Bump mkdocs-macros-plugin from 1.4.0 to 1.4.1. PR [#1626](https://github.com/fastapi/sqlmodel/pull/1626) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.14.1 to 0.14.2. PR [#1616](https://github.com/fastapi/sqlmodel/pull/1616) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1625](https://github.com/fastapi/sqlmodel/pull/1625) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * 🔧 Add PEP-639 license metadata. PR [#1624](https://github.com/fastapi/sqlmodel/pull/1624) by [@svlandeg](https://github.com/svlandeg). From bcd8d1e998b38e9306052a3a3e08969100765c6f Mon Sep 17 00:00:00 2001 From: Adam Collard Date: Tue, 28 Oct 2025 15:42:48 +0000 Subject: [PATCH 809/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20link=20to=20Jet?= =?UTF-8?q?Brains=20Python=20survey=20in=20`features.md`=20(#1627)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update link to JetBrains Python survey in features.md Updated link to the 2024 JetBrains Python Developers Survey. Co-authored-by: Sofie Van Landeghem --- docs/features.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features.md b/docs/features.md index f0d56925c7..2f2e873105 100644 --- a/docs/features.md +++ b/docs/features.md @@ -76,7 +76,7 @@ Underneath, ✨ a **SQLModel** model is also a **SQLAlchemy** model. ✨ There was **a lot** of research and effort dedicated to make it that way. In particular, there was a lot of effort and experimentation in making a single model be **both a SQLAlchemy model and a Pydantic** model at the same time. -That means that you get all the power, robustness, and certainty of SQLAlchemy, the
most widely used database library in Python. +That means that you get all the power, robustness, and certainty of SQLAlchemy, the most widely used database library in Python. **SQLModel** provides its own utilities to improve the developer experience, but underneath, it uses all of SQLAlchemy. From 2bfcad154af3b15ef0c4e07a2a8da5d811ef4018 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 28 Oct 2025 15:43:10 +0000 Subject: [PATCH 810/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 8540bb7b39..ba9dab7c7c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Docs +* 📝 Update link to JetBrains Python survey in `features.md`. PR [#1627](https://github.com/fastapi/sqlmodel/pull/1627) by [@sparkiegeek](https://github.com/sparkiegeek). * 📝 Fix broken links in docs. PR [#1601](https://github.com/fastapi/sqlmodel/pull/1601) by [@YuriiMotov](https://github.com/YuriiMotov). ### Internal From d231476b3f0f8170cf4de927d4652c266f943343 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 09:30:57 +0100 Subject: [PATCH 811/906] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#1636)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.2 → v0.14.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.2...v0.14.3) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 25dcd7b885..8e5eba4c4b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.2 + rev: v0.14.3 hooks: - id: ruff args: From a8f1e16aa2bb3b000c79856cd34549e9489f890d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 09:31:07 +0100 Subject: [PATCH 812/906] =?UTF-8?q?=E2=AC=86=20Bump=20ruff=20from=200.14.2?= =?UTF-8?q?=20to=200.14.3=20(#1633)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.2 to 0.14.3. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.14.2...0.14.3) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.14.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index f301755303..cad202566f 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -5,7 +5,7 @@ coverage[toml] >=6.2,<8.0 # Remove when support for Python 3.8 is dropped mypy ==1.14.1; python_version < "3.9" mypy ==1.18.2; python_version >= "3.9" -ruff ==0.14.2 +ruff ==0.14.3 # For FastAPI tests fastapi >=0.103.2 httpx ==0.28.1 From e5ac98c8e17cde8a0a5892dc074bd0600175ee84 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 4 Nov 2025 08:31:19 +0000 Subject: [PATCH 813/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index ba9dab7c7c..c81f7eab14 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1636](https://github.com/fastapi/sqlmodel/pull/1636) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump mkdocs-macros-plugin from 1.4.0 to 1.4.1. PR [#1626](https://github.com/fastapi/sqlmodel/pull/1626) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.14.1 to 0.14.2. PR [#1616](https://github.com/fastapi/sqlmodel/pull/1616) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1625](https://github.com/fastapi/sqlmodel/pull/1625) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From 553634d0be6bb7fc183ce825d7eebd17b2d40ced Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 09:31:23 +0100 Subject: [PATCH 814/906] =?UTF-8?q?=E2=AC=86=20Bump=20mkdocs-material=20fr?= =?UTF-8?q?om=209.6.22=20to=209.6.23=20(#1637)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.6.22 to 9.6.23. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.6.22...9.6.23) --- updated-dependencies: - dependency-name: mkdocs-material dependency-version: 9.6.23 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index fff91420ff..62dd5dbb4f 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,6 +1,6 @@ -e . -r requirements-docs-tests.txt -mkdocs-material==9.6.22 +mkdocs-material==9.6.23 mdx-include >=1.4.1,<2.0.0 mkdocs-redirects>=1.2.1,<1.3.0 pyyaml >=5.3.1,<7.0.0 From e89e6a56b162c3ec506706db732fab09aae8a096 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 4 Nov 2025 08:32:00 +0000 Subject: [PATCH 815/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index c81f7eab14..6e53b2be83 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* ⬆ Bump ruff from 0.14.2 to 0.14.3. PR [#1633](https://github.com/fastapi/sqlmodel/pull/1633) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1636](https://github.com/fastapi/sqlmodel/pull/1636) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump mkdocs-macros-plugin from 1.4.0 to 1.4.1. PR [#1626](https://github.com/fastapi/sqlmodel/pull/1626) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.14.1 to 0.14.2. PR [#1616](https://github.com/fastapi/sqlmodel/pull/1616) by [@dependabot[bot]](https://github.com/apps/dependabot). From 5c1b89afd9205884c46ff71819b327b00d185a3a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 4 Nov 2025 08:32:56 +0000 Subject: [PATCH 816/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 6e53b2be83..cd80972163 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* ⬆ Bump mkdocs-material from 9.6.22 to 9.6.23. PR [#1637](https://github.com/fastapi/sqlmodel/pull/1637) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.14.2 to 0.14.3. PR [#1633](https://github.com/fastapi/sqlmodel/pull/1633) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1636](https://github.com/fastapi/sqlmodel/pull/1636) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump mkdocs-macros-plugin from 1.4.0 to 1.4.1. PR [#1626](https://github.com/fastapi/sqlmodel/pull/1626) by [@dependabot[bot]](https://github.com/apps/dependabot). From d4fb02bf3856691c5bdab932761cb5ddc6ba5c1b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 11 Nov 2025 17:02:28 +0100 Subject: [PATCH 817/906] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#1642)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.3 → v0.14.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.3...v0.14.4) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8e5eba4c4b..0cb5bf79e8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.3 + rev: v0.14.4 hooks: - id: ruff args: From 37d30a37c6258349052ba94e73aae545fe359e9d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Nov 2025 17:02:37 +0100 Subject: [PATCH 818/906] =?UTF-8?q?=E2=AC=86=20Bump=20ruff=20from=200.14.3?= =?UTF-8?q?=20to=200.14.4=20(#1640)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.3 to 0.14.4. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.14.3...0.14.4) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.14.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index cad202566f..5ad8c877d0 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -5,7 +5,7 @@ coverage[toml] >=6.2,<8.0 # Remove when support for Python 3.8 is dropped mypy ==1.14.1; python_version < "3.9" mypy ==1.18.2; python_version >= "3.9" -ruff ==0.14.3 +ruff ==0.14.4 # For FastAPI tests fastapi >=0.103.2 httpx ==0.28.1 From e91826af65ce97dc4ab58266801e0ed28f556bfc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 11 Nov 2025 16:02:46 +0000 Subject: [PATCH 819/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index cd80972163..cb84b63ed1 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1642](https://github.com/fastapi/sqlmodel/pull/1642) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump mkdocs-material from 9.6.22 to 9.6.23. PR [#1637](https://github.com/fastapi/sqlmodel/pull/1637) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.14.2 to 0.14.3. PR [#1633](https://github.com/fastapi/sqlmodel/pull/1633) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1636](https://github.com/fastapi/sqlmodel/pull/1636) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From 43570910db2d7ab2e5efd96f60a0e2a3a61c5474 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 11 Nov 2025 16:02:58 +0000 Subject: [PATCH 820/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index cb84b63ed1..7fec5b5832 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* ⬆ Bump ruff from 0.14.3 to 0.14.4. PR [#1640](https://github.com/fastapi/sqlmodel/pull/1640) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1642](https://github.com/fastapi/sqlmodel/pull/1642) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump mkdocs-material from 9.6.22 to 9.6.23. PR [#1637](https://github.com/fastapi/sqlmodel/pull/1637) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.14.2 to 0.14.3. PR [#1633](https://github.com/fastapi/sqlmodel/pull/1633) by [@dependabot[bot]](https://github.com/apps/dependabot). From d25e55b08ce43884ee50bc16471043aec98e2a7d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 21:53:59 +0100 Subject: [PATCH 821/906] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#1648)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.4 → v0.14.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.4...v0.14.5) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0cb5bf79e8..014ed6cfdd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.4 + rev: v0.14.5 hooks: - id: ruff args: From 01d6a3ffdb87e908f1b14db88847885f70aafd2a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 21:54:10 +0100 Subject: [PATCH 822/906] =?UTF-8?q?=E2=AC=86=20Bump=20ruff=20from=200.14.4?= =?UTF-8?q?=20to=200.14.5=20(#1646)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.4 to 0.14.5. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.14.4...0.14.5) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.14.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 5ad8c877d0..9e1ec68b7c 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -5,7 +5,7 @@ coverage[toml] >=6.2,<8.0 # Remove when support for Python 3.8 is dropped mypy ==1.14.1; python_version < "3.9" mypy ==1.18.2; python_version >= "3.9" -ruff ==0.14.4 +ruff ==0.14.5 # For FastAPI tests fastapi >=0.103.2 httpx ==0.28.1 From 48e2d7089fa3f97328118b4cd21272658cf7785d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 17 Nov 2025 20:54:26 +0000 Subject: [PATCH 823/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 7fec5b5832..c22605ceb6 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1648](https://github.com/fastapi/sqlmodel/pull/1648) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump ruff from 0.14.3 to 0.14.4. PR [#1640](https://github.com/fastapi/sqlmodel/pull/1640) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1642](https://github.com/fastapi/sqlmodel/pull/1642) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump mkdocs-material from 9.6.22 to 9.6.23. PR [#1637](https://github.com/fastapi/sqlmodel/pull/1637) by [@dependabot[bot]](https://github.com/apps/dependabot). From b6cc1da2f05417ae5bda53e720651e546612702b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 17 Nov 2025 20:58:19 +0000 Subject: [PATCH 824/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index c22605ceb6..fac4333faa 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* ⬆ Bump ruff from 0.14.4 to 0.14.5. PR [#1646](https://github.com/fastapi/sqlmodel/pull/1646) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1648](https://github.com/fastapi/sqlmodel/pull/1648) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump ruff from 0.14.3 to 0.14.4. PR [#1640](https://github.com/fastapi/sqlmodel/pull/1640) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1642](https://github.com/fastapi/sqlmodel/pull/1642) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From d6674ef9366d56250b71d20d30bfe6390f261406 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Nov 2025 09:47:32 +0100 Subject: [PATCH 825/906] =?UTF-8?q?=E2=AC=86=20Bump=20mkdocs-macros-plugin?= =?UTF-8?q?=20from=201.4.1=20to=201.5.0=20(#1647)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mkdocs-macros-plugin](https://github.com/fralau/mkdocs_macros_plugin) from 1.4.1 to 1.5.0. - [Release notes](https://github.com/fralau/mkdocs_macros_plugin/releases) - [Changelog](https://github.com/fralau/mkdocs-macros-plugin/blob/master/CHANGELOG.md) - [Commits](https://github.com/fralau/mkdocs_macros_plugin/compare/v1.4.1...v1.5.0) --- updated-dependencies: - dependency-name: mkdocs-macros-plugin dependency-version: 1.5.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 62dd5dbb4f..f12bbde84a 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -14,5 +14,5 @@ cairosvg==2.8.2 griffe-typingdoc==0.3.0 # For griffe, it formats with black typer == 0.20.0 -mkdocs-macros-plugin==1.4.1 +mkdocs-macros-plugin==1.5.0 markdown-include-variants==0.0.5 From 24ea317c643a66185bca62d4696fd6e2b409fbb2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 18 Nov 2025 08:47:51 +0000 Subject: [PATCH 826/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index fac4333faa..ca5330c735 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* ⬆ Bump mkdocs-macros-plugin from 1.4.1 to 1.5.0. PR [#1647](https://github.com/fastapi/sqlmodel/pull/1647) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.14.4 to 0.14.5. PR [#1646](https://github.com/fastapi/sqlmodel/pull/1646) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1648](https://github.com/fastapi/sqlmodel/pull/1648) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump ruff from 0.14.3 to 0.14.4. PR [#1640](https://github.com/fastapi/sqlmodel/pull/1640) by [@dependabot[bot]](https://github.com/apps/dependabot). From 7e898ff9825f8866abda68182066abc7a8729bff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Nov 2025 09:49:53 +0100 Subject: [PATCH 827/906] =?UTF-8?q?=E2=AC=86=20Bump=20mkdocs-material=20fr?= =?UTF-8?q?om=209.6.23=20to=209.7.0=20(#1645)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.6.23 to 9.7.0. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.6.23...9.7.0) --- updated-dependencies: - dependency-name: mkdocs-material dependency-version: 9.7.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index f12bbde84a..a76936c735 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,6 +1,6 @@ -e . -r requirements-docs-tests.txt -mkdocs-material==9.6.23 +mkdocs-material==9.7.0 mdx-include >=1.4.1,<2.0.0 mkdocs-redirects>=1.2.1,<1.3.0 pyyaml >=5.3.1,<7.0.0 From e4763e3e18fbeea99faf73af81181bed88a7bad5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 18 Nov 2025 08:50:19 +0000 Subject: [PATCH 828/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index ca5330c735..22e98e27c7 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* ⬆ Bump mkdocs-material from 9.6.23 to 9.7.0. PR [#1645](https://github.com/fastapi/sqlmodel/pull/1645) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-macros-plugin from 1.4.1 to 1.5.0. PR [#1647](https://github.com/fastapi/sqlmodel/pull/1647) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.14.4 to 0.14.5. PR [#1646](https://github.com/fastapi/sqlmodel/pull/1646) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1648](https://github.com/fastapi/sqlmodel/pull/1648) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From 4006f7b864d4c1ef302a84d8591015766eea968e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 20 Nov 2025 13:12:37 +0100 Subject: [PATCH 829/906] =?UTF-8?q?=F0=9F=94=A7=20Upgrade=20Material=20for?= =?UTF-8?q?=20MkDocs=20and=20remove=20insiders=20(#1650)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 10 +--------- mkdocs.maybe-insiders.yml => mkdocs.env.yml | 1 - mkdocs.insiders.yml | 7 ------- mkdocs.no-insiders.yml | 0 mkdocs.yml | 8 +++++++- requirements-docs-insiders.txt | 3 --- requirements-docs.txt | 3 ++- scripts/docs.py | 16 +--------------- 8 files changed, 11 insertions(+), 37 deletions(-) rename mkdocs.maybe-insiders.yml => mkdocs.env.yml (79%) delete mode 100644 mkdocs.insiders.yml delete mode 100644 mkdocs.no-insiders.yml delete mode 100644 requirements-docs-insiders.txt diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index d4d4737e99..1b702741db 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -33,12 +33,9 @@ jobs: - docs/** - docs_src/** - requirements-docs.txt - - requirements-docs-insiders.txt - pyproject.toml - mkdocs.yml - - mkdocs.insiders.yml - - mkdocs.maybe-insiders.yml - - mkdocs.no-insiders.yml + - mkdocs.env.yml - .github/workflows/build-docs.yml - .github/workflows/deploy-docs.yml - data/** @@ -68,11 +65,6 @@ jobs: pyproject.toml - name: Install docs extras run: uv pip install -r requirements-docs.txt - - name: Install Material for MkDocs Insiders - if: ( github.event_name != 'pull_request' || github.secret_source == 'Actions' ) - run: uv pip install -r requirements-docs-insiders.txt - env: - TOKEN: ${{ secrets.SQLMODEL_MKDOCS_MATERIAL_INSIDERS }} - uses: actions/cache@v4 with: key: mkdocs-cards-${{ github.ref }} diff --git a/mkdocs.maybe-insiders.yml b/mkdocs.env.yml similarity index 79% rename from mkdocs.maybe-insiders.yml rename to mkdocs.env.yml index 07aefaaa99..545d30a18c 100644 --- a/mkdocs.maybe-insiders.yml +++ b/mkdocs.env.yml @@ -1,6 +1,5 @@ # Define this here and not in the main mkdocs.yml file because that one could be auto # updated and written, and the script would remove the env var -INHERIT: !ENV [INSIDERS_FILE, './mkdocs.no-insiders.yml'] markdown_extensions: pymdownx.highlight: linenums: !ENV [LINENUMS, false] diff --git a/mkdocs.insiders.yml b/mkdocs.insiders.yml deleted file mode 100644 index 80d2d4b640..0000000000 --- a/mkdocs.insiders.yml +++ /dev/null @@ -1,7 +0,0 @@ -plugins: - typeset: -markdown_extensions: - material.extensions.preview: - targets: - include: - - "*" diff --git a/mkdocs.no-insiders.yml b/mkdocs.no-insiders.yml deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/mkdocs.yml b/mkdocs.yml index c59ccd245a..b89516e024 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,4 +1,4 @@ -INHERIT: ./mkdocs.maybe-insiders.yml +INHERIT: ./mkdocs.env.yml site_name: SQLModel site_description: SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness. site_url: https://sqlmodel.tiangolo.com/ @@ -58,6 +58,7 @@ plugins: # Material for MkDocs search: social: + typeset: # Other plugins macros: include_yaml: @@ -140,6 +141,11 @@ nav: - release-notes.md markdown_extensions: + # Material for MkDocs + material.extensions.preview: + targets: + include: + - "*" # Python Markdown abbr: attr_list: diff --git a/requirements-docs-insiders.txt b/requirements-docs-insiders.txt deleted file mode 100644 index d8d3c37a9f..0000000000 --- a/requirements-docs-insiders.txt +++ /dev/null @@ -1,3 +0,0 @@ -git+https://${TOKEN}@github.com/squidfunk/mkdocs-material-insiders.git@9.5.30-insiders-4.53.11 -git+https://${TOKEN}@github.com/pawamoy-insiders/griffe-typing-deprecated.git -git+https://${TOKEN}@github.com/pawamoy-insiders/mkdocstrings-python.git diff --git a/requirements-docs.txt b/requirements-docs.txt index a76936c735..1bee2c6c72 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -10,8 +10,9 @@ pyyaml >=5.3.1,<7.0.0 pillow==11.3.0 # For image processing by Material for MkDocs cairosvg==2.8.2 -# mkdocstrings[python]==0.25.1 +mkdocstrings[python]==0.30.1 griffe-typingdoc==0.3.0 +griffe-warnings-deprecated==1.1.0 # For griffe, it formats with black typer == 0.20.0 mkdocs-macros-plugin==1.5.0 diff --git a/scripts/docs.py b/scripts/docs.py index d018ace86f..a424f177b4 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -2,9 +2,7 @@ import os import re import subprocess -from functools import lru_cache from http.server import HTTPServer, SimpleHTTPRequestHandler -from importlib import metadata from pathlib import Path import mkdocs.utils @@ -19,17 +17,9 @@ app = typer.Typer() -@lru_cache -def is_mkdocs_insiders() -> bool: - version = metadata.version("mkdocs-material") - return "insiders" in version - - @app.callback() def callback() -> None: - if is_mkdocs_insiders(): - os.environ["INSIDERS_FILE"] = "./mkdocs.insiders.yml" - # For MacOS with insiders and Cairo + # For MacOS with Cairo os.environ["DYLD_FALLBACK_LIBRARY_PATH"] = "/opt/homebrew/lib" @@ -126,10 +116,6 @@ def build() -> None: """ Build the docs. """ - insiders_env_file = os.environ.get("INSIDERS_FILE") - print(f"Insiders file {insiders_env_file}") - if is_mkdocs_insiders(): - print("Using insiders") print("Building docs") subprocess.run(["mkdocs", "build"], check=True) typer.secho("Successfully built docs", color=typer.colors.GREEN) From dd1e874fdfc374d1efaa37405c96a991b46fa679 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 20 Nov 2025 12:12:55 +0000 Subject: [PATCH 830/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 22e98e27c7..f2d0a6ccaa 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* 🔧 Upgrade Material for MkDocs and remove insiders. PR [#1650](https://github.com/fastapi/sqlmodel/pull/1650) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump mkdocs-material from 9.6.23 to 9.7.0. PR [#1645](https://github.com/fastapi/sqlmodel/pull/1645) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-macros-plugin from 1.4.1 to 1.5.0. PR [#1647](https://github.com/fastapi/sqlmodel/pull/1647) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.14.4 to 0.14.5. PR [#1646](https://github.com/fastapi/sqlmodel/pull/1646) by [@dependabot[bot]](https://github.com/apps/dependabot). From 49243bd31e63515651af0d4a3030670ecabb7df0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Nov 2025 10:05:47 +0100 Subject: [PATCH 831/906] =?UTF-8?q?=E2=AC=86=20Bump=20actions/checkout=20f?= =?UTF-8?q?rom=205=20to=206=20(#1651)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-docs.yml | 4 ++-- .github/workflows/deploy-docs.yml | 2 +- .github/workflows/latest-changes.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/smokeshow.yml | 2 +- .github/workflows/test-redistribute.yml | 2 +- .github/workflows/test.yml | 4 ++-- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 1b702741db..5a419ce2b2 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -22,7 +22,7 @@ jobs: outputs: docs: ${{ steps.filter.outputs.docs }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 # For pull requests it's not necessary to checkout the code but for the main branch it is - uses: dorny/paths-filter@v3 id: filter @@ -50,7 +50,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 05e5a0685c..b0977c5bbd 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -23,7 +23,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml index 08802c6528..6b34beb5e5 100644 --- a/.github/workflows/latest-changes.yml +++ b/.github/workflows/latest-changes.yml @@ -20,7 +20,7 @@ jobs: latest-changes: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: # To allow latest-changes to commit to the main branch token: ${{ secrets.SQLMODEL_LATEST_CHANGES }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b35142f50e..05543b2f15 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -22,7 +22,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index 79d1ded7ee..a53e4a087f 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -16,7 +16,7 @@ jobs: if: ${{ github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: python-version: '3.9' diff --git a/.github/workflows/test-redistribute.yml b/.github/workflows/test-redistribute.yml index 2a7ff20d31..1a51b367d6 100644 --- a/.github/workflows/test-redistribute.yml +++ b/.github/workflows/test-redistribute.yml @@ -22,7 +22,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8392d92438..641c8829c6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -54,7 +54,7 @@ jobs: fail-fast: false runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: @@ -102,7 +102,7 @@ jobs: - test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: python-version: '3.13' From 16bd6cdb84327a38c1d6d8cd259dbc50945bbc04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 23 Nov 2025 01:27:49 -0800 Subject: [PATCH 832/906] =?UTF-8?q?=F0=9F=92=84=20Use=20font=20Fira=20Code?= =?UTF-8?q?=20to=20fix=20display=20of=20Rich=20panels=20in=20docs=20in=20W?= =?UTF-8?q?indows=20(#1653)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/css/custom.css | 8 ++++++++ docs/css/termynal.css | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/css/custom.css b/docs/css/custom.css index 200ac45cd6..7833d66fbe 100644 --- a/docs/css/custom.css +++ b/docs/css/custom.css @@ -1,3 +1,11 @@ +/* Fira Code, including characters used by Rich output, like the "heavy right-pointing angle bracket ornament", not included in Google Fonts */ +@import url(https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/fira_code.css); + +/* Override default code font in Material for MkDocs to Fira Code */ +:root { + --md-code-font: "Fira Code", monospace; +} + .termynal-comment { color: #4a968f; font-style: italic; diff --git a/docs/css/termynal.css b/docs/css/termynal.css index 8534f91021..a2564e2860 100644 --- a/docs/css/termynal.css +++ b/docs/css/termynal.css @@ -20,7 +20,7 @@ /* font-size: 18px; */ font-size: 15px; /* font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; */ - font-family: 'Roboto Mono', 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; + font-family: var(--md-code-font-family), 'Roboto Mono', 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; border-radius: 4px; padding: 75px 45px 35px; position: relative; From 152cf064c20a75a78a8155016b1134ac2bb92599 Mon Sep 17 00:00:00 2001 From: Sofie Van Landeghem Date: Mon, 24 Nov 2025 15:50:25 +0100 Subject: [PATCH 833/906] =?UTF-8?q?=F0=9F=91=B7=20Upgrade=20`latest-change?= =?UTF-8?q?s`=20GitHub=20Action=20and=20pin=20`actions/checkout@v5`=20(#16?= =?UTF-8?q?54)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 👷 Upgrade latest-changes and pin actions/checkout@v5 --- .github/workflows/latest-changes.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml index 6b34beb5e5..08ee8cf603 100644 --- a/.github/workflows/latest-changes.yml +++ b/.github/workflows/latest-changes.yml @@ -20,7 +20,9 @@ jobs: latest-changes: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + # pin to actions/checkout@v5 for compatibility with latest-changes + # Ref: https://github.com/actions/checkout/issues/2313 + - uses: actions/checkout@v5 with: # To allow latest-changes to commit to the main branch token: ${{ secrets.SQLMODEL_LATEST_CHANGES }} @@ -30,7 +32,7 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true - - uses: tiangolo/latest-changes@0.4.0 + - uses: tiangolo/latest-changes@0.4.1 with: token: ${{ secrets.GITHUB_TOKEN }} latest_changes_file: docs/release-notes.md From 1179124b65bbea5d040ee91b00a1cd5e71b04b94 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 24 Nov 2025 14:50:43 +0000 Subject: [PATCH 834/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index f2d0a6ccaa..56d02e7977 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* 👷 Upgrade `latest-changes` GitHub Action and pin `actions/checkout@v5`. PR [#1654](https://github.com/fastapi/sqlmodel/pull/1654) by [@svlandeg](https://github.com/svlandeg). * 🔧 Upgrade Material for MkDocs and remove insiders. PR [#1650](https://github.com/fastapi/sqlmodel/pull/1650) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump mkdocs-material from 9.6.23 to 9.7.0. PR [#1645](https://github.com/fastapi/sqlmodel/pull/1645) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-macros-plugin from 1.4.1 to 1.5.0. PR [#1647](https://github.com/fastapi/sqlmodel/pull/1647) by [@dependabot[bot]](https://github.com/apps/dependabot). From 51d96f955867cd6ee2abfecbdd7c85c307bec0e9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 24 Nov 2025 14:55:28 +0000 Subject: [PATCH 835/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 56d02e7977..66380dbcc8 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* 💄 Use font Fira Code to fix display of Rich panels in docs in Windows. PR [#1653](https://github.com/fastapi/sqlmodel/pull/1653) by [@tiangolo](https://github.com/tiangolo). * 👷 Upgrade `latest-changes` GitHub Action and pin `actions/checkout@v5`. PR [#1654](https://github.com/fastapi/sqlmodel/pull/1654) by [@svlandeg](https://github.com/svlandeg). * 🔧 Upgrade Material for MkDocs and remove insiders. PR [#1650](https://github.com/fastapi/sqlmodel/pull/1650) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump mkdocs-material from 9.6.23 to 9.7.0. PR [#1645](https://github.com/fastapi/sqlmodel/pull/1645) by [@dependabot[bot]](https://github.com/apps/dependabot). From 14954575e495e150779c7be2a9de6a426dc419e6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 24 Nov 2025 14:55:47 +0000 Subject: [PATCH 836/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 66380dbcc8..76c282aea8 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* ⬆ Bump actions/checkout from 5 to 6. PR [#1651](https://github.com/fastapi/sqlmodel/pull/1651) by [@dependabot[bot]](https://github.com/apps/dependabot). * 💄 Use font Fira Code to fix display of Rich panels in docs in Windows. PR [#1653](https://github.com/fastapi/sqlmodel/pull/1653) by [@tiangolo](https://github.com/tiangolo). * 👷 Upgrade `latest-changes` GitHub Action and pin `actions/checkout@v5`. PR [#1654](https://github.com/fastapi/sqlmodel/pull/1654) by [@svlandeg](https://github.com/svlandeg). * 🔧 Upgrade Material for MkDocs and remove insiders. PR [#1650](https://github.com/fastapi/sqlmodel/pull/1650) by [@tiangolo](https://github.com/tiangolo). From 194314cea4607d34d57212c2285632f30474bb05 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 18:53:30 +0100 Subject: [PATCH 837/906] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#1655)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.5 → v0.14.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.5...v0.14.6) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 014ed6cfdd..183f632dd4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.5 + rev: v0.14.6 hooks: - id: ruff args: From 2dbe1b5b869ef257a16d75f4d1e51270b556e980 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 18:53:38 +0100 Subject: [PATCH 838/906] =?UTF-8?q?=E2=AC=86=20Bump=20ruff=20from=200.14.5?= =?UTF-8?q?=20to=200.14.6=20(#1652)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.5 to 0.14.6. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.14.5...0.14.6) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.14.6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 9e1ec68b7c..b6531e3c73 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -5,7 +5,7 @@ coverage[toml] >=6.2,<8.0 # Remove when support for Python 3.8 is dropped mypy ==1.14.1; python_version < "3.9" mypy ==1.18.2; python_version >= "3.9" -ruff ==0.14.5 +ruff ==0.14.6 # For FastAPI tests fastapi >=0.103.2 httpx ==0.28.1 From b22e50b2c0742dc658554107adf6b75404327461 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 24 Nov 2025 17:54:11 +0000 Subject: [PATCH 839/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 76c282aea8..75b475e56c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1655](https://github.com/fastapi/sqlmodel/pull/1655) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump actions/checkout from 5 to 6. PR [#1651](https://github.com/fastapi/sqlmodel/pull/1651) by [@dependabot[bot]](https://github.com/apps/dependabot). * 💄 Use font Fira Code to fix display of Rich panels in docs in Windows. PR [#1653](https://github.com/fastapi/sqlmodel/pull/1653) by [@tiangolo](https://github.com/tiangolo). * 👷 Upgrade `latest-changes` GitHub Action and pin `actions/checkout@v5`. PR [#1654](https://github.com/fastapi/sqlmodel/pull/1654) by [@svlandeg](https://github.com/svlandeg). From 97ce8250dbe33e37fe11d02edb3a3f20e58ae761 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 24 Nov 2025 17:54:22 +0000 Subject: [PATCH 840/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 75b475e56c..be7490e65b 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* ⬆ Bump ruff from 0.14.5 to 0.14.6. PR [#1652](https://github.com/fastapi/sqlmodel/pull/1652) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1655](https://github.com/fastapi/sqlmodel/pull/1655) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump actions/checkout from 5 to 6. PR [#1651](https://github.com/fastapi/sqlmodel/pull/1651) by [@dependabot[bot]](https://github.com/apps/dependabot). * 💄 Use font Fira Code to fix display of Rich panels in docs in Windows. PR [#1653](https://github.com/fastapi/sqlmodel/pull/1653) by [@tiangolo](https://github.com/tiangolo). From 4e65c3bb11097348a7fcd009c331c1cfaf34bdcd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:27:28 +0100 Subject: [PATCH 841/906] =?UTF-8?q?=E2=AC=86=20Bump=20actions/checkout=20f?= =?UTF-8?q?rom=205=20to=206=20(#1656)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ⬆ Bump actions/checkout from 5 to 6 Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * Remove comment - issue was fixed --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sofie Van Landeghem --- .github/workflows/latest-changes.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml index 08ee8cf603..bdfaa4b2ad 100644 --- a/.github/workflows/latest-changes.yml +++ b/.github/workflows/latest-changes.yml @@ -20,9 +20,7 @@ jobs: latest-changes: runs-on: ubuntu-latest steps: - # pin to actions/checkout@v5 for compatibility with latest-changes - # Ref: https://github.com/actions/checkout/issues/2313 - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: # To allow latest-changes to commit to the main branch token: ${{ secrets.SQLMODEL_LATEST_CHANGES }} From dbfd1891fb96c2078c43a1c11061c3e0257d1029 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 25 Nov 2025 10:27:48 +0000 Subject: [PATCH 842/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index be7490e65b..d41f1611d7 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Internal +* ⬆ Bump actions/checkout from 5 to 6. PR [#1656](https://github.com/fastapi/sqlmodel/pull/1656) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.14.5 to 0.14.6. PR [#1652](https://github.com/fastapi/sqlmodel/pull/1652) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1655](https://github.com/fastapi/sqlmodel/pull/1655) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump actions/checkout from 5 to 6. PR [#1651](https://github.com/fastapi/sqlmodel/pull/1651) by [@dependabot[bot]](https://github.com/apps/dependabot). From 8838801bdd4fe8d0670c06d0df907cf2322b473c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 28 Nov 2025 07:54:48 -0800 Subject: [PATCH 843/906] =?UTF-8?q?=F0=9F=92=85=20Update=20CSS=20to=20expl?= =?UTF-8?q?icitly=20use=20emoji=20font=20(#1658)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/css/custom.css | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/css/custom.css b/docs/css/custom.css index 7833d66fbe..6ac827ff07 100644 --- a/docs/css/custom.css +++ b/docs/css/custom.css @@ -1,9 +1,16 @@ /* Fira Code, including characters used by Rich output, like the "heavy right-pointing angle bracket ornament", not included in Google Fonts */ @import url(https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/fira_code.css); +/* Noto Color Emoji for emoji support with the same font everywhere */ +@import url(https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&display=swap); /* Override default code font in Material for MkDocs to Fira Code */ :root { - --md-code-font: "Fira Code", monospace; + --md-code-font: "Fira Code", monospace, "Noto Color Emoji"; +} + +/* Override default regular font in Material for MkDocs to include Noto Color Emoji */ +:root { + --md-text-font: "Roboto", "Noto Color Emoji"; } .termynal-comment { From 3e971b2dc190d60431e9f579fa2bd62ad7353110 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 28 Nov 2025 15:55:18 +0000 Subject: [PATCH 844/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index d41f1611d7..ad24203483 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Docs +* 💅 Update CSS to explicitly use emoji font. PR [#1658](https://github.com/fastapi/sqlmodel/pull/1658) by [@tiangolo](https://github.com/tiangolo). * 📝 Update link to JetBrains Python survey in `features.md`. PR [#1627](https://github.com/fastapi/sqlmodel/pull/1627) by [@sparkiegeek](https://github.com/sparkiegeek). * 📝 Fix broken links in docs. PR [#1601](https://github.com/fastapi/sqlmodel/pull/1601) by [@YuriiMotov](https://github.com/YuriiMotov). From 7bda52fbd7d15a769459003046ebd8f0cda91d3f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 21:33:18 +0530 Subject: [PATCH 845/906] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#1662)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.6 → v0.14.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.6...v0.14.8) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 183f632dd4..584ba3cbee 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.6 + rev: v0.14.8 hooks: - id: ruff args: From 73bb5b3821a64f4ee6703a4babac5c169f932106 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 21:33:38 +0530 Subject: [PATCH 846/906] =?UTF-8?q?=E2=AC=86=20Bump=20ruff=20from=200.14.6?= =?UTF-8?q?=20to=200.14.8=20(#1667)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.6 to 0.14.8. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.14.6...0.14.8) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.14.8 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index b6531e3c73..5510da3c34 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -5,7 +5,7 @@ coverage[toml] >=6.2,<8.0 # Remove when support for Python 3.8 is dropped mypy ==1.14.1; python_version < "3.9" mypy ==1.18.2; python_version >= "3.9" -ruff ==0.14.6 +ruff ==0.14.8 # For FastAPI tests fastapi >=0.103.2 httpx ==0.28.1 From 2a9bf7b4cfe4d76bad042544d3c62d16caa1b573 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 10 Dec 2025 16:03:40 +0000 Subject: [PATCH 847/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index ad24203483..d080d78d2c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,6 +10,7 @@ ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1662](https://github.com/fastapi/sqlmodel/pull/1662) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump actions/checkout from 5 to 6. PR [#1656](https://github.com/fastapi/sqlmodel/pull/1656) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.14.5 to 0.14.6. PR [#1652](https://github.com/fastapi/sqlmodel/pull/1652) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1655](https://github.com/fastapi/sqlmodel/pull/1655) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From bb101707ab0c7568ee2f22faed94614eda1cd5b2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 10 Dec 2025 16:03:58 +0000 Subject: [PATCH 848/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index d080d78d2c..d338244b56 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,6 +10,7 @@ ### Internal +* ⬆ Bump ruff from 0.14.6 to 0.14.8. PR [#1667](https://github.com/fastapi/sqlmodel/pull/1667) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1662](https://github.com/fastapi/sqlmodel/pull/1662) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump actions/checkout from 5 to 6. PR [#1656](https://github.com/fastapi/sqlmodel/pull/1656) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.14.5 to 0.14.6. PR [#1652](https://github.com/fastapi/sqlmodel/pull/1652) by [@dependabot[bot]](https://github.com/apps/dependabot). From 8554b57efc416283d1d7c8599ff00219c53b76a5 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Fri, 19 Dec 2025 19:13:52 +0100 Subject: [PATCH 849/906] =?UTF-8?q?=F0=9F=91=B7=20Run=20Smokeshow=20always?= =?UTF-8?q?,=20even=20on=20test=20failures=20(#1682)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/smokeshow.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index a53e4a087f..6eeb2cbbfd 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -13,18 +13,15 @@ env: jobs: smokeshow: - if: ${{ github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: - python-version: '3.9' + python-version: '3.13' - name: Setup uv uses: astral-sh/setup-uv@v7 with: - version: "0.4.15" - enable-cache: true cache-dependency-glob: | requirements**.txt pyproject.toml @@ -48,7 +45,7 @@ jobs: done env: SMOKESHOW_GITHUB_STATUS_DESCRIPTION: Coverage {coverage-percentage} - SMOKESHOW_GITHUB_COVERAGE_THRESHOLD: 95 + SMOKESHOW_GITHUB_COVERAGE_THRESHOLD: 99 SMOKESHOW_GITHUB_CONTEXT: coverage SMOKESHOW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SMOKESHOW_GITHUB_PR_HEAD_SHA: ${{ github.event.workflow_run.head_sha }} From 90613a451f4cdb74ee17e1a4b3408e72f2f8e231 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 19 Dec 2025 18:14:14 +0000 Subject: [PATCH 850/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index d338244b56..55a799c9c0 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,6 +10,7 @@ ### Internal +* 👷 Run Smokeshow always, even on test failures. PR [#1682](https://github.com/fastapi/sqlmodel/pull/1682) by [@YuriiMotov](https://github.com/YuriiMotov). * ⬆ Bump ruff from 0.14.6 to 0.14.8. PR [#1667](https://github.com/fastapi/sqlmodel/pull/1667) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1662](https://github.com/fastapi/sqlmodel/pull/1662) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump actions/checkout from 5 to 6. PR [#1656](https://github.com/fastapi/sqlmodel/pull/1656) by [@dependabot[bot]](https://github.com/apps/dependabot). From ba5e97d97d1807a88fc47b24ecb9cb3d04928145 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Fri, 19 Dec 2025 19:16:32 +0100 Subject: [PATCH 851/906] =?UTF-8?q?=F0=9F=91=B7=20Configure=20coverage,=20?= =?UTF-8?q?error=20on=20main=20tests,=20don't=20wait=20for=20Smokeshow=20(?= =?UTF-8?q?#1683)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 641c8829c6..dadb437fbb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -124,7 +124,6 @@ jobs: run: uv pip install -r requirements-tests.txt - run: ls -la coverage - run: coverage combine coverage - - run: coverage report - run: coverage html --title "Coverage for ${{ github.sha }}" - name: Store coverage HTML uses: actions/upload-artifact@v5 @@ -132,6 +131,7 @@ jobs: name: coverage-html path: htmlcov include-hidden-files: true + - run: coverage report --fail-under=99 # https://github.com/marketplace/actions/alls-green#why alls-green: # This job does nothing and is only used for the branch protection From 9262835b663a8a880f040ddd4af0f60c59c6f66a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 19 Dec 2025 18:16:49 +0000 Subject: [PATCH 852/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 55a799c9c0..93a3723e8b 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,6 +10,7 @@ ### Internal +* 👷 Configure coverage, error on main tests, don't wait for Smokeshow. PR [#1683](https://github.com/fastapi/sqlmodel/pull/1683) by [@YuriiMotov](https://github.com/YuriiMotov). * 👷 Run Smokeshow always, even on test failures. PR [#1682](https://github.com/fastapi/sqlmodel/pull/1682) by [@YuriiMotov](https://github.com/YuriiMotov). * ⬆ Bump ruff from 0.14.6 to 0.14.8. PR [#1667](https://github.com/fastapi/sqlmodel/pull/1667) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1662](https://github.com/fastapi/sqlmodel/pull/1662) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From 09f3b7f34cfe8a9f48c2559a288cdae5d04933d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 23 Dec 2025 08:04:25 -0800 Subject: [PATCH 853/906] =?UTF-8?q?=F0=9F=93=8C=20Pin=20FastAPI=20in=20tes?= =?UTF-8?q?ts=20to=200.125.0=20while=20dropping=20support=20for=20Python?= =?UTF-8?q?=203.8=20(#1689)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 5510da3c34..4094a006c8 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -7,7 +7,7 @@ mypy ==1.14.1; python_version < "3.9" mypy ==1.18.2; python_version >= "3.9" ruff ==0.14.8 # For FastAPI tests -fastapi >=0.103.2 +fastapi >=0.103.2,<0.126.0 httpx ==0.28.1 dirty-equals ==0.9.0 jinja2 ==3.1.6 From fb331e7ce7798cb0a7d9b2a3e934191ffabfb4a8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 23 Dec 2025 16:04:41 +0000 Subject: [PATCH 854/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 93a3723e8b..04243171e7 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,6 +10,7 @@ ### Internal +* 📌 Pin FastAPI in tests to 0.125.0 while dropping support for Python 3.8. PR [#1689](https://github.com/fastapi/sqlmodel/pull/1689) by [@tiangolo](https://github.com/tiangolo). * 👷 Configure coverage, error on main tests, don't wait for Smokeshow. PR [#1683](https://github.com/fastapi/sqlmodel/pull/1683) by [@YuriiMotov](https://github.com/YuriiMotov). * 👷 Run Smokeshow always, even on test failures. PR [#1682](https://github.com/fastapi/sqlmodel/pull/1682) by [@YuriiMotov](https://github.com/YuriiMotov). * ⬆ Bump ruff from 0.14.6 to 0.14.8. PR [#1667](https://github.com/fastapi/sqlmodel/pull/1667) by [@dependabot[bot]](https://github.com/apps/dependabot). From 6cec19c8dcd867d940b773d6e3f6e2dca316cb0f Mon Sep 17 00:00:00 2001 From: Bart Schuurmans Date: Tue, 23 Dec 2025 17:23:54 +0100 Subject: [PATCH 855/906] =?UTF-8?q?=F0=9F=90=9B=20Fix=20`RuntimeError:=20d?= =?UTF-8?q?ictionary=20changed=20size=20during=20iteration`=20in=20`sqlmod?= =?UTF-8?q?el=5Fupdate()`=20(#997)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> --- sqlmodel/main.py | 3 +-- tests/test_update.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 tests/test_update.py diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 7c916f79af..f168fc02ac 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -1004,9 +1004,8 @@ def sqlmodel_update( else: value = getattr(obj, key) setattr(self, key, value) - for remaining_key in use_update: + for remaining_key, value in use_update.items(): if remaining_key in get_model_fields(self): - value = use_update.pop(remaining_key) setattr(self, remaining_key, value) else: raise ValueError( diff --git a/tests/test_update.py b/tests/test_update.py new file mode 100644 index 0000000000..de4bd6cdd2 --- /dev/null +++ b/tests/test_update.py @@ -0,0 +1,20 @@ +from sqlmodel import Field, SQLModel + + +def test_sqlmodel_update(): + class Organization(SQLModel, table=True): + id: int = Field(default=None, primary_key=True) + name: str + headquarters: str + + class OrganizationUpdate(SQLModel): + name: str + + org = Organization(name="Example Org", city="New York", headquarters="NYC HQ") + org_in = OrganizationUpdate(name="Updated org") + org.sqlmodel_update( + org_in, + update={ + "headquarters": "-", # This field is in Organization, but not in OrganizationUpdate + }, + ) From f8873ef1b7906ea0af8a1cdbee40264ac3691995 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 23 Dec 2025 16:24:13 +0000 Subject: [PATCH 856/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 04243171e7..23b96cca83 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Fixes + +* 🐛 Fix `RuntimeError: dictionary changed size during iteration` in `sqlmodel_update()`. PR [#997](https://github.com/fastapi/sqlmodel/pull/997) by [@BartSchuurmans](https://github.com/BartSchuurmans). + ### Docs * 💅 Update CSS to explicitly use emoji font. PR [#1658](https://github.com/fastapi/sqlmodel/pull/1658) by [@tiangolo](https://github.com/tiangolo). From 59adbe76a6b06e09bbdc815a8aea055a1885af22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 23 Dec 2025 17:39:28 +0100 Subject: [PATCH 857/906] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.0.?= =?UTF-8?q?28?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 2 ++ sqlmodel/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 23b96cca83..32b4ff4491 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest Changes +## 0.0.28 + ### Fixes * 🐛 Fix `RuntimeError: dictionary changed size during iteration` in `sqlmodel_update()`. PR [#997](https://github.com/fastapi/sqlmodel/pull/997) by [@BartSchuurmans](https://github.com/BartSchuurmans). diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index 9175458980..788fbb71be 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.0.27" +__version__ = "0.0.28" # Re-export from SQLAlchemy from sqlalchemy.engine import create_engine as create_engine From 5b1d8e6e7da7cf31d5adf2696cebed338a65cfc3 Mon Sep 17 00:00:00 2001 From: Ravishankar Sivasubramaniam Date: Tue, 23 Dec 2025 14:57:43 -0600 Subject: [PATCH 858/906] =?UTF-8?q?=F0=9F=90=9B=20Fix=20`alias`=20support?= =?UTF-8?q?=20for=20Pydantic=20v2=20(#1577)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Yurii Motov Co-authored-by: Sebastián Ramírez --- sqlmodel/main.py | 94 +++++++++------ tests/test_aliases.py | 257 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 318 insertions(+), 33 deletions(-) create mode 100644 tests/test_aliases.py diff --git a/sqlmodel/main.py b/sqlmodel/main.py index f168fc02ac..d301694260 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -218,6 +218,8 @@ def Field( *, default_factory: Optional[NoArgAnyCallable] = None, alias: Optional[str] = None, + validation_alias: Optional[str] = None, + serialization_alias: Optional[str] = None, title: Optional[str] = None, description: Optional[str] = None, exclude: Union[ @@ -263,6 +265,8 @@ def Field( *, default_factory: Optional[NoArgAnyCallable] = None, alias: Optional[str] = None, + validation_alias: Optional[str] = None, + serialization_alias: Optional[str] = None, title: Optional[str] = None, description: Optional[str] = None, exclude: Union[ @@ -317,6 +321,8 @@ def Field( *, default_factory: Optional[NoArgAnyCallable] = None, alias: Optional[str] = None, + validation_alias: Optional[str] = None, + serialization_alias: Optional[str] = None, title: Optional[str] = None, description: Optional[str] = None, exclude: Union[ @@ -352,6 +358,8 @@ def Field( *, default_factory: Optional[NoArgAnyCallable] = None, alias: Optional[str] = None, + validation_alias: Optional[str] = None, + serialization_alias: Optional[str] = None, title: Optional[str] = None, description: Optional[str] = None, exclude: Union[ @@ -390,43 +398,63 @@ def Field( schema_extra: Optional[Dict[str, Any]] = None, ) -> Any: current_schema_extra = schema_extra or {} + # Extract possible alias settings from schema_extra so we can control precedence + schema_validation_alias = current_schema_extra.pop("validation_alias", None) + schema_serialization_alias = current_schema_extra.pop("serialization_alias", None) + field_info_kwargs = { + "alias": alias, + "title": title, + "description": description, + "exclude": exclude, + "include": include, + "const": const, + "gt": gt, + "ge": ge, + "lt": lt, + "le": le, + "multiple_of": multiple_of, + "max_digits": max_digits, + "decimal_places": decimal_places, + "min_items": min_items, + "max_items": max_items, + "unique_items": unique_items, + "min_length": min_length, + "max_length": max_length, + "allow_mutation": allow_mutation, + "regex": regex, + "discriminator": discriminator, + "repr": repr, + "primary_key": primary_key, + "foreign_key": foreign_key, + "ondelete": ondelete, + "unique": unique, + "nullable": nullable, + "index": index, + "sa_type": sa_type, + "sa_column": sa_column, + "sa_column_args": sa_column_args, + "sa_column_kwargs": sa_column_kwargs, + **current_schema_extra, + } + if IS_PYDANTIC_V2: + # explicit params > schema_extra > alias propagation + field_info_kwargs["validation_alias"] = ( + validation_alias or schema_validation_alias or alias + ) + field_info_kwargs["serialization_alias"] = ( + serialization_alias or schema_serialization_alias or alias + ) + else: + if validation_alias or schema_validation_alias is not None: + raise RuntimeError("validation_alias is not supported in Pydantic v1") + if serialization_alias or schema_serialization_alias is not None: + raise RuntimeError("serialization_alias is not supported in Pydantic v1") field_info = FieldInfo( default, default_factory=default_factory, - alias=alias, - title=title, - description=description, - exclude=exclude, - include=include, - const=const, - gt=gt, - ge=ge, - lt=lt, - le=le, - multiple_of=multiple_of, - max_digits=max_digits, - decimal_places=decimal_places, - min_items=min_items, - max_items=max_items, - unique_items=unique_items, - min_length=min_length, - max_length=max_length, - allow_mutation=allow_mutation, - regex=regex, - discriminator=discriminator, - repr=repr, - primary_key=primary_key, - foreign_key=foreign_key, - ondelete=ondelete, - unique=unique, - nullable=nullable, - index=index, - sa_type=sa_type, - sa_column=sa_column, - sa_column_args=sa_column_args, - sa_column_kwargs=sa_column_kwargs, - **current_schema_extra, + **field_info_kwargs, ) + post_init_field_info(field_info) return field_info diff --git a/tests/test_aliases.py b/tests/test_aliases.py new file mode 100644 index 0000000000..2ac9a5acd7 --- /dev/null +++ b/tests/test_aliases.py @@ -0,0 +1,257 @@ +from typing import Type, Union + +import pytest +from pydantic import BaseModel, ValidationError +from pydantic import Field as PField +from sqlmodel import Field, SQLModel +from sqlmodel._compat import IS_PYDANTIC_V2 + +from tests.conftest import needs_pydanticv1, needs_pydanticv2 + +""" +Alias tests for SQLModel and Pydantic compatibility +""" + + +class PydanticUser(BaseModel): + full_name: str = PField(alias="fullName") + + +class SQLModelUser(SQLModel): + full_name: str = Field(alias="fullName") + + +# Models with config (validate_by_name=True) +if IS_PYDANTIC_V2: + + class PydanticUserWithConfig(PydanticUser): + model_config = {"validate_by_name": True} + + class SQLModelUserWithConfig(SQLModelUser): + model_config = {"validate_by_name": True} + +else: + + class PydanticUserWithConfig(PydanticUser): + class Config: + allow_population_by_field_name = True + + class SQLModelUserWithConfig(SQLModelUser): + class Config: + allow_population_by_field_name = True + + +@pytest.mark.parametrize("model", [PydanticUser, SQLModelUser]) +def test_create_with_field_name(model: Union[Type[PydanticUser], Type[SQLModelUser]]): + with pytest.raises(ValidationError): + model(full_name="Alice") + + +@pytest.mark.parametrize("model", [PydanticUserWithConfig, SQLModelUserWithConfig]) +def test_create_with_field_name_with_config( + model: Union[Type[PydanticUserWithConfig], Type[SQLModelUserWithConfig]], +): + user = model(full_name="Alice") + assert user.full_name == "Alice" + + +@pytest.mark.parametrize( + "model", + [PydanticUser, SQLModelUser, PydanticUserWithConfig, SQLModelUserWithConfig], +) +def test_create_with_alias( + model: Union[ + Type[PydanticUser], + Type[SQLModelUser], + Type[PydanticUserWithConfig], + Type[SQLModelUserWithConfig], + ], +): + user = model(fullName="Bob") # using alias + assert user.full_name == "Bob" + + +@pytest.mark.parametrize("model", [PydanticUserWithConfig, SQLModelUserWithConfig]) +def test_create_with_both_prefers_alias( + model: Union[Type[PydanticUserWithConfig], Type[SQLModelUserWithConfig]], +): + user = model(full_name="IGNORED", fullName="Charlie") + assert user.full_name == "Charlie" # alias should take precedence + + +@pytest.mark.parametrize("model", [PydanticUser, SQLModelUser]) +def test_dict_default_uses_field_names( + model: Union[Type[PydanticUser], Type[SQLModelUser]], +): + user = model(fullName="Dana") + if IS_PYDANTIC_V2 or isinstance(user, SQLModel): + data = user.model_dump() + else: + data = user.dict() + assert "full_name" in data + assert "fullName" not in data + assert data["full_name"] == "Dana" + + +@pytest.mark.parametrize("model", [PydanticUser, SQLModelUser]) +def test_dict_by_alias_uses_aliases( + model: Union[Type[PydanticUser], Type[SQLModelUser]], +): + user = model(fullName="Dana") + if IS_PYDANTIC_V2 or isinstance(user, SQLModel): + data = user.model_dump(by_alias=True) + else: + data = user.dict(by_alias=True) + assert "fullName" in data + assert "full_name" not in data + assert data["fullName"] == "Dana" + + +@pytest.mark.parametrize("model", [PydanticUser, SQLModelUser]) +def test_json_by_alias( + model: Union[Type[PydanticUser], Type[SQLModelUser]], +): + user = model(fullName="Frank") + if IS_PYDANTIC_V2: + json_data = user.model_dump_json(by_alias=True) + else: + json_data = user.json(by_alias=True) + assert ('"fullName":"Frank"' in json_data) or ('"fullName": "Frank"' in json_data) + assert "full_name" not in json_data + + +if IS_PYDANTIC_V2: + + class PydanticUserV2(BaseModel): + first_name: str = PField( + validation_alias="firstName", serialization_alias="f_name" + ) + + class SQLModelUserV2(SQLModel): + first_name: str = Field( + validation_alias="firstName", serialization_alias="f_name" + ) +else: + # Dummy classes for Pydantic v1 to prevent import errors + PydanticUserV2 = None + SQLModelUserV2 = None + + +@needs_pydanticv1 +def test_validation_alias_runtimeerror_pydantic_v1(): + with pytest.raises( + RuntimeError, match="validation_alias is not supported in Pydantic v1" + ): + Field(validation_alias="foo") + + +@needs_pydanticv1 +def test_serialization_alias_runtimeerror_pydantic_v1(): + with pytest.raises( + RuntimeError, match="serialization_alias is not supported in Pydantic v1" + ): + Field(serialization_alias="bar") + + +@needs_pydanticv2 +@pytest.mark.parametrize("model", [PydanticUserV2, SQLModelUserV2]) +def test_create_with_validation_alias( + model: Union[Type[PydanticUserV2], Type[SQLModelUserV2]], +): + user = model(firstName="John") + assert user.first_name == "John" + + +@needs_pydanticv2 +@pytest.mark.parametrize("model", [PydanticUserV2, SQLModelUserV2]) +def test_serialize_with_serialization_alias( + model: Union[Type[PydanticUserV2], Type[SQLModelUserV2]], +): + user = model(firstName="Jane") + data = user.model_dump(by_alias=True) + assert "f_name" in data + assert "firstName" not in data + assert "first_name" not in data + assert data["f_name"] == "Jane" + + +@needs_pydanticv2 +def test_schema_extra_validation_alias_sqlmodel_v2(): + class M(SQLModel): + f: str = Field(schema_extra={"validation_alias": "f_alias"}) + + m = M.model_validate({"f_alias": "asd"}) + assert m.f == "asd" + + +@needs_pydanticv2 +def test_schema_extra_serialization_alias_sqlmodel_v2(): + class M(SQLModel): + f: str = Field(schema_extra={"serialization_alias": "f_out"}) + + m = M(f="x") + data = m.model_dump(by_alias=True) + assert "f_out" in data + assert "f" not in data + assert data["f_out"] == "x" + + +@needs_pydanticv1 +def test_schema_extra_validation_alias_runtimeerror_pydantic_v1(): + with pytest.raises( + RuntimeError, match="validation_alias is not supported in Pydantic v1" + ): + Field(schema_extra={"validation_alias": "x"}) + + +@needs_pydanticv1 +def test_schema_extra_serialization_alias_runtimeerror_pydantic_v1(): + with pytest.raises( + RuntimeError, match="serialization_alias is not supported in Pydantic v1" + ): + Field(schema_extra={"serialization_alias": "y"}) + + +@needs_pydanticv2 +def test_alias_plus_validation_alias_prefers_validation_alias_sqlmodel_v2(): + class M(SQLModel): + first_name: str = Field(alias="fullName", validation_alias="v_name") + + m = M.model_validate({"fullName": "A", "v_name": "B"}) + assert m.first_name == "B" + + +@needs_pydanticv2 +def test_alias_plus_serialization_alias_prefers_serialization_alias_sqlmodel_v2(): + class M(SQLModel): + first_name: str = Field(alias="fullName", serialization_alias="f_name") + + m = M(fullName="Z") + data = m.model_dump(by_alias=True) + assert "f_name" in data + assert "fullName" not in data + assert data["f_name"] == "Z" + + +@needs_pydanticv2 +def test_alias_generator_works_sqlmodel_v2(): + class M(SQLModel): + model_config = {"alias_generator": lambda s: "gen_" + s} + f: str = Field() + + m = M.model_validate({"gen_f": "ok"}) + assert m.f == "ok" + data = m.model_dump(by_alias=True) + assert "gen_f" in data and data["gen_f"] == "ok" + + +@needs_pydanticv2 +def test_alias_generator_with_explicit_alias_prefers_field_alias_sqlmodel_v2(): + class M(SQLModel): + model_config = {"alias_generator": lambda s: "gen_" + s} + f: str = Field(alias="custom") + + m = M.model_validate({"custom": "ok"}) + assert m.f == "ok" + data = m.model_dump(by_alias=True) + assert "custom" in data and "gen_f" not in data From 44e7a2d1ac54fbebefcc4ea1334e920dfc48a428 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 23 Dec 2025 20:58:01 +0000 Subject: [PATCH 859/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 32b4ff4491..1c29abfc9c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Fixes + +* 🐛 Fix `alias` support for Pydantic v2. PR [#1577](https://github.com/fastapi/sqlmodel/pull/1577) by [@ravishan16](https://github.com/ravishan16). + ## 0.0.28 ### Fixes From c702e2ec3e3490fca925e66a03ad8cf9c09c5b5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 23 Dec 2025 21:58:42 +0100 Subject: [PATCH 860/906] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.0.?= =?UTF-8?q?29?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 2 ++ sqlmodel/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 1c29abfc9c..00f61eca4f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest Changes +## 0.0.29 + ### Fixes * 🐛 Fix `alias` support for Pydantic v2. PR [#1577](https://github.com/fastapi/sqlmodel/pull/1577) by [@ravishan16](https://github.com/ravishan16). diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index 788fbb71be..00499bba3e 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.0.28" +__version__ = "0.0.29" # Re-export from SQLAlchemy from sqlalchemy.engine import create_engine as create_engine From 1248baea6c923bab69820e8ab528473919d07a28 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Dec 2025 09:03:49 +0100 Subject: [PATCH 861/906] =?UTF-8?q?=E2=AC=86=20Bump=20ruff=20from=200.14.8?= =?UTF-8?q?=20to=200.14.10=20(#1681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.8 to 0.14.10. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.14.8...0.14.10) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.14.10 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 4094a006c8..843d618ac6 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -5,7 +5,7 @@ coverage[toml] >=6.2,<8.0 # Remove when support for Python 3.8 is dropped mypy ==1.14.1; python_version < "3.9" mypy ==1.18.2; python_version >= "3.9" -ruff ==0.14.8 +ruff ==0.14.10 # For FastAPI tests fastapi >=0.103.2,<0.126.0 httpx ==0.28.1 From e9eff378a7135d1707dc5ea5af4211045e38bcba Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 24 Dec 2025 08:04:10 +0000 Subject: [PATCH 862/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 00f61eca4f..4482009494 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Internal + +* ⬆ Bump ruff from 0.14.8 to 0.14.10. PR [#1681](https://github.com/fastapi/sqlmodel/pull/1681) by [@dependabot[bot]](https://github.com/apps/dependabot). + ## 0.0.29 ### Fixes From 18511b44bd97e55bc1a9baf81c80555f576ab48c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Dec 2025 09:05:41 +0100 Subject: [PATCH 863/906] =?UTF-8?q?=E2=AC=86=20Bump=20typer=20from=200.20.?= =?UTF-8?q?0=20to=200.20.1=20(#1685)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [typer](https://github.com/fastapi/typer) from 0.20.0 to 0.20.1. - [Release notes](https://github.com/fastapi/typer/releases) - [Changelog](https://github.com/fastapi/typer/blob/master/docs/release-notes.md) - [Commits](https://github.com/fastapi/typer/compare/0.20.0...0.20.1) --- updated-dependencies: - dependency-name: typer dependency-version: 0.20.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 1bee2c6c72..61283abe00 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -14,6 +14,6 @@ mkdocstrings[python]==0.30.1 griffe-typingdoc==0.3.0 griffe-warnings-deprecated==1.1.0 # For griffe, it formats with black -typer == 0.20.0 +typer == 0.20.1 mkdocs-macros-plugin==1.5.0 markdown-include-variants==0.0.5 From 1aa90931f51d905e02d6880cd48ade8954efa681 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 24 Dec 2025 08:06:05 +0000 Subject: [PATCH 864/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 4482009494..459bb5ce6b 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Internal +* ⬆ Bump typer from 0.20.0 to 0.20.1. PR [#1685](https://github.com/fastapi/sqlmodel/pull/1685) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.14.8 to 0.14.10. PR [#1681](https://github.com/fastapi/sqlmodel/pull/1681) by [@dependabot[bot]](https://github.com/apps/dependabot). ## 0.0.29 From 9da57b37c69ef7e29b91ce21fbb1e800e7733642 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Dec 2025 09:11:04 +0100 Subject: [PATCH 865/906] =?UTF-8?q?=E2=AC=86=20Bump=20mypy=20from=201.18.2?= =?UTF-8?q?=20to=201.19.1=20(#1679)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mypy](https://github.com/python/mypy) from 1.18.2 to 1.19.1. - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.18.2...v1.19.1) --- updated-dependencies: - dependency-name: mypy dependency-version: 1.19.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 843d618ac6..936791db50 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -4,7 +4,7 @@ pytest >=7.0.1,<9.0.0 coverage[toml] >=6.2,<8.0 # Remove when support for Python 3.8 is dropped mypy ==1.14.1; python_version < "3.9" -mypy ==1.18.2; python_version >= "3.9" +mypy ==1.19.1; python_version >= "3.9" ruff ==0.14.10 # For FastAPI tests fastapi >=0.103.2,<0.126.0 From 807c6de93ef5b5dbcf229dd4dc6798a0c9d12ab7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 24 Dec 2025 08:11:26 +0000 Subject: [PATCH 866/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 459bb5ce6b..a2a94f79ae 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Internal +* ⬆ Bump mypy from 1.18.2 to 1.19.1. PR [#1679](https://github.com/fastapi/sqlmodel/pull/1679) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump typer from 0.20.0 to 0.20.1. PR [#1685](https://github.com/fastapi/sqlmodel/pull/1685) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.14.8 to 0.14.10. PR [#1681](https://github.com/fastapi/sqlmodel/pull/1681) by [@dependabot[bot]](https://github.com/apps/dependabot). From 4ff6f5042372e235b42a5f06c200470e9acdaf12 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Dec 2025 09:15:05 +0100 Subject: [PATCH 867/906] =?UTF-8?q?=E2=AC=86=20Bump=20actions/upload-artif?= =?UTF-8?q?act=20from=205=20to=206=20(#1675)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> --- .github/workflows/build-docs.yml | 2 +- .github/workflows/test.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 5a419ce2b2..3b6c5fb33e 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -73,7 +73,7 @@ jobs: run: python ./scripts/docs.py verify-readme - name: Build Docs run: python ./scripts/docs.py build - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@v6 with: name: docs-site path: ./site/** diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dadb437fbb..328dfe1921 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -91,7 +91,7 @@ jobs: COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}-${{ matrix.pydantic-version }} CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }} - name: Store coverage files - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: coverage-${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.pydantic-version }} path: coverage @@ -126,7 +126,7 @@ jobs: - run: coverage combine coverage - run: coverage html --title "Coverage for ${{ github.sha }}" - name: Store coverage HTML - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: coverage-html path: htmlcov From d82f636c18a6ec131c9a00c1db4d1f03f1f59a38 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 24 Dec 2025 08:15:20 +0000 Subject: [PATCH 868/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index a2a94f79ae..c925ff704b 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Internal +* ⬆ Bump actions/upload-artifact from 5 to 6. PR [#1675](https://github.com/fastapi/sqlmodel/pull/1675) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mypy from 1.18.2 to 1.19.1. PR [#1679](https://github.com/fastapi/sqlmodel/pull/1679) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump typer from 0.20.0 to 0.20.1. PR [#1685](https://github.com/fastapi/sqlmodel/pull/1685) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump ruff from 0.14.8 to 0.14.10. PR [#1681](https://github.com/fastapi/sqlmodel/pull/1681) by [@dependabot[bot]](https://github.com/apps/dependabot). From 2cfa68c06ed9ab56a3a4c849e629c90fd003a2bd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Dec 2025 09:16:00 +0100 Subject: [PATCH 869/906] =?UTF-8?q?=E2=AC=86=20Bump=20markdown-include-var?= =?UTF-8?q?iants=20from=200.0.5=20to=200.0.8=20(#1674)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [markdown-include-variants](https://github.com/tiangolo/markdown-include-variants) from 0.0.5 to 0.0.8. - [Release notes](https://github.com/tiangolo/markdown-include-variants/releases) - [Changelog](https://github.com/tiangolo/markdown-include-variants/blob/main/release-notes.md) - [Commits](https://github.com/tiangolo/markdown-include-variants/compare/0.0.5...0.0.8) --- updated-dependencies: - dependency-name: markdown-include-variants dependency-version: 0.0.8 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 61283abe00..e4978793e0 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -16,4 +16,4 @@ griffe-warnings-deprecated==1.1.0 # For griffe, it formats with black typer == 0.20.1 mkdocs-macros-plugin==1.5.0 -markdown-include-variants==0.0.5 +markdown-include-variants==0.0.8 From 4145e503c8156f1bbac09307336d79c81cb20a96 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 24 Dec 2025 08:16:25 +0000 Subject: [PATCH 870/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index c925ff704b..eeafdb1953 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Internal +* ⬆ Bump markdown-include-variants from 0.0.5 to 0.0.8. PR [#1674](https://github.com/fastapi/sqlmodel/pull/1674) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/upload-artifact from 5 to 6. PR [#1675](https://github.com/fastapi/sqlmodel/pull/1675) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mypy from 1.18.2 to 1.19.1. PR [#1679](https://github.com/fastapi/sqlmodel/pull/1679) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump typer from 0.20.0 to 0.20.1. PR [#1685](https://github.com/fastapi/sqlmodel/pull/1685) by [@dependabot[bot]](https://github.com/apps/dependabot). From 8b80f96555a6ea087ac75463f0d6a0b4057169ca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Dec 2025 09:18:23 +0100 Subject: [PATCH 871/906] =?UTF-8?q?=E2=AC=86=20Bump=20actions/cache=20from?= =?UTF-8?q?=204=20to=205=20(#1673)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/cache dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 3b6c5fb33e..29767ef998 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -65,7 +65,7 @@ jobs: pyproject.toml - name: Install docs extras run: uv pip install -r requirements-docs.txt - - uses: actions/cache@v4 + - uses: actions/cache@v5 with: key: mkdocs-cards-${{ github.ref }} path: .cache From dad906f2b484ec79c0181d9bf336253f0ddde673 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 24 Dec 2025 08:18:40 +0000 Subject: [PATCH 872/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index eeafdb1953..c3643f1e71 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Internal +* ⬆ Bump actions/cache from 4 to 5. PR [#1673](https://github.com/fastapi/sqlmodel/pull/1673) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump markdown-include-variants from 0.0.5 to 0.0.8. PR [#1674](https://github.com/fastapi/sqlmodel/pull/1674) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/upload-artifact from 5 to 6. PR [#1675](https://github.com/fastapi/sqlmodel/pull/1675) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mypy from 1.18.2 to 1.19.1. PR [#1679](https://github.com/fastapi/sqlmodel/pull/1679) by [@dependabot[bot]](https://github.com/apps/dependabot). From 59d8c4836151c4021716df3245dcc1d49812b27a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Dec 2025 09:21:25 +0100 Subject: [PATCH 873/906] =?UTF-8?q?=E2=AC=86=20Bump=20actions/download-art?= =?UTF-8?q?ifact=20from=206=20to=207=20(#1676)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 6 to 7. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/deploy-docs.yml | 2 +- .github/workflows/smokeshow.yml | 2 +- .github/workflows/test.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index b0977c5bbd..519e4ae667 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -49,7 +49,7 @@ jobs: run: | rm -rf ./site mkdir ./site - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v7 with: path: ./site/ pattern: docs-site diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index 6eeb2cbbfd..c4b72d4b65 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -26,7 +26,7 @@ jobs: requirements**.txt pyproject.toml - run: uv pip install -r requirements-github-actions.txt - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v7 with: name: coverage-html path: htmlcov diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 328dfe1921..9738ecf570 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -115,7 +115,7 @@ jobs: requirements**.txt pyproject.toml - name: Get coverage files - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: pattern: coverage-* path: coverage From f81ca3c73e7fbce50ee03016c5ba1ff756b0f8df Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 24 Dec 2025 08:21:49 +0000 Subject: [PATCH 874/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index c3643f1e71..e7e467f52a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Internal +* ⬆ Bump actions/download-artifact from 6 to 7. PR [#1676](https://github.com/fastapi/sqlmodel/pull/1676) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/cache from 4 to 5. PR [#1673](https://github.com/fastapi/sqlmodel/pull/1673) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump markdown-include-variants from 0.0.5 to 0.0.8. PR [#1674](https://github.com/fastapi/sqlmodel/pull/1674) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/upload-artifact from 5 to 6. PR [#1675](https://github.com/fastapi/sqlmodel/pull/1675) by [@dependabot[bot]](https://github.com/apps/dependabot). From d0cbbc67cb3c08e1be1e3545ac32ce5701663c16 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 24 Dec 2025 09:27:41 +0100 Subject: [PATCH 875/906] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commi?= =?UTF-8?q?t=20autoupdate=20(#1677)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.8 → v0.14.10](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.8...v0.14.10) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 584ba3cbee..916fb47935 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.8 + rev: v0.14.10 hooks: - id: ruff args: From 4c4d5b44ff5dcd30531beceb662ac0b2b235cbe3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 24 Dec 2025 08:28:00 +0000 Subject: [PATCH 876/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index e7e467f52a..cc30b327be 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1677](https://github.com/fastapi/sqlmodel/pull/1677) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump actions/download-artifact from 6 to 7. PR [#1676](https://github.com/fastapi/sqlmodel/pull/1676) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/cache from 4 to 5. PR [#1673](https://github.com/fastapi/sqlmodel/pull/1673) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump markdown-include-variants from 0.0.5 to 0.0.8. PR [#1674](https://github.com/fastapi/sqlmodel/pull/1674) by [@dependabot[bot]](https://github.com/apps/dependabot). From 4ea6a32f6f379fe511c3225a633bb90db24587ae Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Wed, 24 Dec 2025 16:47:05 +0100 Subject: [PATCH 877/906] =?UTF-8?q?=E2=9C=85=20Simplify=20tests=20for=20co?= =?UTF-8?q?de=20examples,=20one=20test=20file=20for=20multiple=20variants?= =?UTF-8?q?=20(#1664)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../test_decimal/test_tutorial001.py | 36 +- .../test_decimal/test_tutorial001_py310.py | 45 - .../test_uuid/test_tutorial001.py | 31 +- .../test_uuid/test_tutorial001_py310.py | 72 -- .../test_uuid/test_tutorial002.py | 31 +- .../test_uuid/test_tutorial002_py310.py | 72 -- .../test_code_structure/test_tutorial002.py | 47 +- .../test_tutorial002_py310.py | 38 - .../test_tutorial002_py39.py | 38 - .../test_tutorial001.py | 34 +- .../test_tutorial001_py310.py | 17 - .../test_delete/test_tutorial001.py | 33 +- .../test_delete/test_tutorial001_py310.py | 73 -- .../test_insert/test_tutorial001.py | 37 +- .../test_insert/test_tutorial001_py310.py | 53 -- ...est_tutorial001_py310_tutorial002_py310.py | 92 --- .../test_tutorial001_tutorial002.py | 58 +- .../test_select/test_tutorial003.py | 33 +- .../test_select/test_tutorial003_py310.py | 89 -- .../test_select/test_tutorial004.py | 33 +- .../test_select/test_tutorial004_py310.py | 63 -- .../test_select/test_tutorial005.py | 33 +- .../test_select/test_tutorial005_py310.py | 65 -- .../test_update/test_tutorial001.py | 33 +- .../test_update/test_tutorial001_py310.py | 63 -- .../test_tutorial001.py | 15 +- .../test_tutorial001_py310.py | 19 - .../test_tutorial002.py | 32 +- .../test_tutorial002_py310.py | 16 - .../test_tutorial003.py | 32 +- .../test_tutorial003_py310.py | 16 - ...est_tutorial001_py310_tutorial002_py310.py | 88 -- .../test_tutorial001_tutorial002.py | 56 +- .../test_tutorial001_py310_tests_main.py | 25 - .../test_tutorial001_py39_tests_main.py | 25 - .../test_tutorial001_tests001.py | 42 +- .../test_tutorial001_tests002.py | 42 +- .../test_tutorial001_tests003.py | 42 +- .../test_tutorial001_tests004.py | 42 +- .../test_tutorial001_tests_main.py | 25 +- .../test_delete/test_tutorial001.py | 22 +- .../test_delete/test_tutorial001_py310.py | 377 --------- .../test_delete/test_tutorial001_py39.py | 377 --------- .../test_limit_and_offset/test_tutorial001.py | 24 +- .../test_tutorial001_py310.py | 274 ------- .../test_tutorial001_py39.py | 274 ------- .../test_multiple_models/test_tutorial001.py | 28 +- .../test_tutorial001_py310.py | 220 ----- .../test_tutorial001_py39.py | 221 ----- .../test_multiple_models/test_tutorial002.py | 28 +- .../test_tutorial002_py310.py | 221 ----- .../test_tutorial002_py39.py | 221 ----- .../test_read_one/test_tutorial001.py | 22 +- .../test_read_one/test_tutorial001_py310.py | 219 ----- .../test_read_one/test_tutorial001_py39.py | 219 ----- .../test_relationships/test_tutorial001.py | 24 +- .../test_tutorial001_py310.py | 767 ------------------ .../test_tutorial001_py39.py | 767 ------------------ .../test_response_model/test_tutorial001.py | 24 +- .../test_tutorial001_py310.py | 162 ---- .../test_tutorial001_py39.py | 162 ---- .../test_tutorial001.py | 24 +- .../test_tutorial001_py310.py | 379 --------- .../test_tutorial001_py39.py | 379 --------- .../test_simple_hero_api/test_tutorial001.py | 23 +- .../test_tutorial001_py310.py | 168 ---- .../test_teams/test_tutorial001.py | 22 +- .../test_teams/test_tutorial001_py310.py | 686 ---------------- .../test_teams/test_tutorial001_py39.py | 686 ---------------- .../test_update/test_tutorial001.py | 22 +- .../test_update/test_tutorial001_py310.py | 356 -------- .../test_update/test_tutorial001_py39.py | 356 -------- .../test_update/test_tutorial002.py | 42 +- .../test_update/test_tutorial002_py310.py | 430 ---------- .../test_update/test_tutorial002_py39.py | 430 ---------- .../test_indexes/test_tutorial001.py | 27 +- .../test_indexes/test_tutorial001_py310.py | 46 -- .../test_indexes/test_tutorial002.py | 27 +- .../test_indexes/test_tutorial002_py310.py | 47 -- .../test_insert/test_tutorial001.py | 20 +- .../test_insert/test_tutorial001_py310.py | 30 - .../test_insert/test_tutorial002.py | 20 +- .../test_insert/test_tutorial002_py310.py | 30 - .../test_insert/test_tutorial003.py | 20 +- .../test_insert/test_tutorial003_py310.py | 30 - .../test_limit_and_offset/test_tutorial001.py | 36 +- .../test_tutorial001_py310.py | 35 - .../test_limit_and_offset/test_tutorial002.py | 36 +- .../test_tutorial002_py310.py | 35 - .../test_limit_and_offset/test_tutorial003.py | 36 +- .../test_tutorial003_py310.py | 33 - .../test_limit_and_offset/test_tutorial004.py | 27 +- .../test_tutorial004_py310.py | 27 - .../test_many_to_many/test_tutorial001.py | 37 +- .../test_tutorial001_py310.py | 50 -- .../test_tutorial001_py39.py | 50 -- .../test_many_to_many/test_tutorial002.py | 37 +- .../test_tutorial002_py310.py | 77 -- .../test_tutorial002_py39.py | 77 -- .../test_many_to_many/test_tutorial003.py | 37 +- .../test_tutorial003_py310.py | 73 -- .../test_tutorial003_py39.py | 73 -- .../test_one/test_tutorial001.py | 27 +- .../test_one/test_tutorial001_py310.py | 30 - .../test_one/test_tutorial002.py | 27 +- .../test_one/test_tutorial002_py310.py | 20 - .../test_one/test_tutorial003.py | 27 +- .../test_one/test_tutorial003_py310.py | 25 - .../test_one/test_tutorial004.py | 30 +- .../test_one/test_tutorial004_py310.py | 41 - .../test_one/test_tutorial005.py | 30 +- .../test_one/test_tutorial005_py310.py | 41 - .../test_one/test_tutorial006.py | 27 +- .../test_one/test_tutorial006_py310.py | 25 - .../test_one/test_tutorial007.py | 27 +- .../test_one/test_tutorial007_py310.py | 25 - .../test_one/test_tutorial008.py | 27 +- .../test_one/test_tutorial008_py310.py | 25 - .../test_one/test_tutorial009.py | 27 +- .../test_one/test_tutorial009_py310.py | 20 - .../test_back_populates/test_tutorial001.py | 42 +- .../test_tutorial001_py310.py | 290 ------- .../test_tutorial001_py39.py | 290 ------- .../test_back_populates/test_tutorial002.py | 41 +- .../test_tutorial002_py310.py | 280 ------- .../test_tutorial002_py39.py | 280 ------- .../test_back_populates/test_tutorial003.py | 25 +- .../test_tutorial003_py310.py | 21 - .../test_tutorial003_py39.py | 21 - .../test_tutorial001.py | 41 +- .../test_tutorial001_py310.py | 99 --- .../test_tutorial001_py39.py | 99 --- .../test_tutorial001.py | 41 +- .../test_tutorial001_py310.py | 55 -- .../test_tutorial001_py39.py | 55 -- .../test_tutorial001.py | 30 +- .../test_tutorial001_py310.py | 73 -- .../test_tutorial001_py39.py | 73 -- .../test_tutorial002.py | 30 +- .../test_tutorial002_py310.py | 91 --- .../test_tutorial002_py39.py | 91 --- .../test_tutorial003.py | 30 +- .../test_tutorial003_py310.py | 91 --- .../test_tutorial003_py39.py | 91 --- .../test_tutorial004.py | 47 +- .../test_tutorial004_py310.py | 107 --- .../test_tutorial004_py39.py | 107 --- .../test_tutorial005.py | 30 +- .../test_tutorial005_py310.py | 95 --- .../test_tutorial005_py39.py | 95 --- .../test_tutorial001.py | 41 +- .../test_tutorial001_py310.py | 107 --- .../test_tutorial001_py39.py | 107 --- .../test_tutorial002.py | 41 +- .../test_tutorial002_py310.py | 149 ---- .../test_tutorial002_py39.py | 149 ---- ...est_tutorial001_py310_tutorial002_py310.py | 57 -- .../test_tutorial001_tutorial002.py | 64 +- ...est_tutorial003_py310_tutorial004_py310.py | 59 -- .../test_tutorial003_tutorial004.py | 56 +- ...est_tutorial001_py310_tutorial002_py310.py | 56 -- .../test_tutorial001_tutorial002.py | 56 +- ...est_tutorial003_py310_tutorial004_py310.py | 69 -- .../test_tutorial003_tutorial004.py | 56 +- .../test_where/test_tutorial001.py | 27 +- .../test_where/test_tutorial001_py310.py | 29 - .../test_where/test_tutorial002.py | 27 +- .../test_where/test_tutorial002_py310.py | 30 - .../test_where/test_tutorial003.py | 26 +- .../test_where/test_tutorial003_py310.py | 37 - .../test_where/test_tutorial004.py | 26 +- .../test_where/test_tutorial004_py310.py | 37 - .../test_where/test_tutorial005.py | 27 +- .../test_where/test_tutorial005_py310.py | 22 - .../test_where/test_tutorial006.py | 27 +- .../test_where/test_tutorial006_py310.py | 23 - .../test_where/test_tutorial007.py | 27 +- .../test_where/test_tutorial007_py310.py | 23 - .../test_where/test_tutorial008.py | 27 +- .../test_where/test_tutorial008_py310.py | 23 - .../test_where/test_tutorial009.py | 27 +- .../test_where/test_tutorial009_py310.py | 31 - .../test_where/test_tutorial010.py | 27 +- .../test_where/test_tutorial010_py310.py | 31 - .../test_where/test_tutorial011.py | 26 +- .../test_where/test_tutorial011_py310.py | 37 - 186 files changed, 1776 insertions(+), 14635 deletions(-) delete mode 100644 tests/test_advanced/test_decimal/test_tutorial001_py310.py delete mode 100644 tests/test_advanced/test_uuid/test_tutorial001_py310.py delete mode 100644 tests/test_advanced/test_uuid/test_tutorial002_py310.py delete mode 100644 tests/test_tutorial/test_code_structure/test_tutorial002_py310.py delete mode 100644 tests/test_tutorial/test_code_structure/test_tutorial002_py39.py delete mode 100644 tests/test_tutorial/test_connect/test_create_connected_tables/test_tutorial001_py310.py delete mode 100644 tests/test_tutorial/test_connect/test_delete/test_tutorial001_py310.py delete mode 100644 tests/test_tutorial/test_connect/test_insert/test_tutorial001_py310.py delete mode 100644 tests/test_tutorial/test_connect/test_select/test_tutorial001_py310_tutorial002_py310.py delete mode 100644 tests/test_tutorial/test_connect/test_select/test_tutorial003_py310.py delete mode 100644 tests/test_tutorial/test_connect/test_select/test_tutorial004_py310.py delete mode 100644 tests/test_tutorial/test_connect/test_select/test_tutorial005_py310.py delete mode 100644 tests/test_tutorial/test_connect/test_update/test_tutorial001_py310.py delete mode 100644 tests/test_tutorial/test_create_db_and_table/test_tutorial001_py310.py delete mode 100644 tests/test_tutorial/test_create_db_and_table/test_tutorial002_py310.py delete mode 100644 tests/test_tutorial/test_create_db_and_table/test_tutorial003_py310.py delete mode 100644 tests/test_tutorial/test_delete/test_tutorial001_py310_tutorial002_py310.py delete mode 100644 tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_py310_tests_main.py delete mode 100644 tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_py39_tests_main.py delete mode 100644 tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py310.py delete mode 100644 tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py39.py delete mode 100644 tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py310.py delete mode 100644 tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py39.py delete mode 100644 tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py310.py delete mode 100644 tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py39.py delete mode 100644 tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py310.py delete mode 100644 tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py39.py delete mode 100644 tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py310.py delete mode 100644 tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py39.py delete mode 100644 tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py310.py delete mode 100644 tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py39.py delete mode 100644 tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py310.py delete mode 100644 tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py39.py delete mode 100644 tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py310.py delete mode 100644 tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py39.py delete mode 100644 tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001_py310.py delete mode 100644 tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py310.py delete mode 100644 tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py39.py delete mode 100644 tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py310.py delete mode 100644 tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py39.py delete mode 100644 tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py310.py delete mode 100644 tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py39.py delete mode 100644 tests/test_tutorial/test_indexes/test_tutorial001_py310.py delete mode 100644 tests/test_tutorial/test_indexes/test_tutorial002_py310.py delete mode 100644 tests/test_tutorial/test_insert/test_tutorial001_py310.py delete mode 100644 tests/test_tutorial/test_insert/test_tutorial002_py310.py delete mode 100644 tests/test_tutorial/test_insert/test_tutorial003_py310.py delete mode 100644 tests/test_tutorial/test_limit_and_offset/test_tutorial001_py310.py delete mode 100644 tests/test_tutorial/test_limit_and_offset/test_tutorial002_py310.py delete mode 100644 tests/test_tutorial/test_limit_and_offset/test_tutorial003_py310.py delete mode 100644 tests/test_tutorial/test_limit_and_offset/test_tutorial004_py310.py delete mode 100644 tests/test_tutorial/test_many_to_many/test_tutorial001_py310.py delete mode 100644 tests/test_tutorial/test_many_to_many/test_tutorial001_py39.py delete mode 100644 tests/test_tutorial/test_many_to_many/test_tutorial002_py310.py delete mode 100644 tests/test_tutorial/test_many_to_many/test_tutorial002_py39.py delete mode 100644 tests/test_tutorial/test_many_to_many/test_tutorial003_py310.py delete mode 100644 tests/test_tutorial/test_many_to_many/test_tutorial003_py39.py delete mode 100644 tests/test_tutorial/test_one/test_tutorial001_py310.py delete mode 100644 tests/test_tutorial/test_one/test_tutorial002_py310.py delete mode 100644 tests/test_tutorial/test_one/test_tutorial003_py310.py delete mode 100644 tests/test_tutorial/test_one/test_tutorial004_py310.py delete mode 100644 tests/test_tutorial/test_one/test_tutorial005_py310.py delete mode 100644 tests/test_tutorial/test_one/test_tutorial006_py310.py delete mode 100644 tests/test_tutorial/test_one/test_tutorial007_py310.py delete mode 100644 tests/test_tutorial/test_one/test_tutorial008_py310.py delete mode 100644 tests/test_tutorial/test_one/test_tutorial009_py310.py delete mode 100644 tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001_py310.py delete mode 100644 tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001_py39.py delete mode 100644 tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002_py310.py delete mode 100644 tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002_py39.py delete mode 100644 tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003_py310.py delete mode 100644 tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003_py39.py delete mode 100644 tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001_py310.py delete mode 100644 tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001_py39.py delete mode 100644 tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001_py310.py delete mode 100644 tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001_py39.py delete mode 100644 tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001_py310.py delete mode 100644 tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001_py39.py delete mode 100644 tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002_py310.py delete mode 100644 tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002_py39.py delete mode 100644 tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003_py310.py delete mode 100644 tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003_py39.py delete mode 100644 tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004_py310.py delete mode 100644 tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004_py39.py delete mode 100644 tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005_py310.py delete mode 100644 tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005_py39.py delete mode 100644 tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001_py310.py delete mode 100644 tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001_py39.py delete mode 100644 tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002_py310.py delete mode 100644 tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002_py39.py delete mode 100644 tests/test_tutorial/test_select/test_tutorial001_py310_tutorial002_py310.py delete mode 100644 tests/test_tutorial/test_select/test_tutorial003_py310_tutorial004_py310.py delete mode 100644 tests/test_tutorial/test_update/test_tutorial001_py310_tutorial002_py310.py delete mode 100644 tests/test_tutorial/test_update/test_tutorial003_py310_tutorial004_py310.py delete mode 100644 tests/test_tutorial/test_where/test_tutorial001_py310.py delete mode 100644 tests/test_tutorial/test_where/test_tutorial002_py310.py delete mode 100644 tests/test_tutorial/test_where/test_tutorial003_py310.py delete mode 100644 tests/test_tutorial/test_where/test_tutorial004_py310.py delete mode 100644 tests/test_tutorial/test_where/test_tutorial005_py310.py delete mode 100644 tests/test_tutorial/test_where/test_tutorial006_py310.py delete mode 100644 tests/test_tutorial/test_where/test_tutorial007_py310.py delete mode 100644 tests/test_tutorial/test_where/test_tutorial008_py310.py delete mode 100644 tests/test_tutorial/test_where/test_tutorial009_py310.py delete mode 100644 tests/test_tutorial/test_where/test_tutorial010_py310.py delete mode 100644 tests/test_tutorial/test_where/test_tutorial011_py310.py diff --git a/tests/test_advanced/test_decimal/test_tutorial001.py b/tests/test_advanced/test_decimal/test_tutorial001.py index 2dc562209f..dad8f24c4c 100644 --- a/tests/test_advanced/test_decimal/test_tutorial001.py +++ b/tests/test_advanced/test_decimal/test_tutorial001.py @@ -1,9 +1,26 @@ +import importlib from decimal import Decimal -from unittest.mock import patch +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 + + +@pytest.fixture( + name="mod", + params=[ + "tutorial001", + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.advanced.decimal.{request.param}") + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + return mod + expected_calls = [ [ @@ -30,15 +47,6 @@ ] -def test_tutorial(): - from docs_src.advanced.decimal import tutorial001 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_advanced/test_decimal/test_tutorial001_py310.py b/tests/test_advanced/test_decimal/test_tutorial001_py310.py deleted file mode 100644 index 4cda8b4653..0000000000 --- a/tests/test_advanced/test_decimal/test_tutorial001_py310.py +++ /dev/null @@ -1,45 +0,0 @@ -from decimal import Decimal -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Hero 1:", - { - "name": "Deadpond", - "age": None, - "id": 1, - "secret_name": "Dive Wilson", - "money": Decimal("1.100"), - }, - ], - [ - "Hero 2:", - { - "name": "Rusty-Man", - "age": 48, - "id": 3, - "secret_name": "Tommy Sharp", - "money": Decimal("2.200"), - }, - ], - ["Total money: 3.300"], -] - - -@needs_py310 -def test_tutorial(): - from docs_src.advanced.decimal import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_advanced/test_uuid/test_tutorial001.py b/tests/test_advanced/test_uuid/test_tutorial001.py index b9d5a36800..ab4f3387dc 100644 --- a/tests/test_advanced/test_uuid/test_tutorial001.py +++ b/tests/test_advanced/test_uuid/test_tutorial001.py @@ -1,31 +1,38 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from dirty_equals import IsUUID from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 -def test_tutorial() -> None: - from docs_src.advanced.uuid import tutorial001 as mod - +@pytest.fixture( + name="mod", + params=[ + "tutorial001", + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.advanced.uuid.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - first_uuid = calls[1][0]["id"] +def test_tutorial(print_mock: PrintMock, mod: ModuleType) -> None: + mod.main() + first_uuid = print_mock.calls[1][0]["id"] assert first_uuid == IsUUID(4) - second_uuid = calls[7][0]["id"] + second_uuid = print_mock.calls[7][0]["id"] assert second_uuid == IsUUID(4) assert first_uuid != second_uuid - assert calls == [ + assert print_mock.calls == [ ["The hero before saving in the DB"], [ { diff --git a/tests/test_advanced/test_uuid/test_tutorial001_py310.py b/tests/test_advanced/test_uuid/test_tutorial001_py310.py deleted file mode 100644 index 1250c32872..0000000000 --- a/tests/test_advanced/test_uuid/test_tutorial001_py310.py +++ /dev/null @@ -1,72 +0,0 @@ -from unittest.mock import patch - -from dirty_equals import IsUUID -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial() -> None: - from docs_src.advanced.uuid import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - first_uuid = calls[1][0]["id"] - assert first_uuid == IsUUID(4) - - second_uuid = calls[7][0]["id"] - assert second_uuid == IsUUID(4) - - assert first_uuid != second_uuid - - assert calls == [ - ["The hero before saving in the DB"], - [ - { - "name": "Deadpond", - "secret_name": "Dive Wilson", - "id": first_uuid, - "age": None, - } - ], - ["The hero ID was already set"], - [first_uuid], - ["After saving in the DB"], - [ - { - "name": "Deadpond", - "secret_name": "Dive Wilson", - "age": None, - "id": first_uuid, - } - ], - ["Created hero:"], - [ - { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "age": None, - "id": second_uuid, - } - ], - ["Created hero ID:"], - [second_uuid], - ["Selected hero:"], - [ - { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "age": None, - "id": second_uuid, - } - ], - ["Selected hero ID:"], - [second_uuid], - ] diff --git a/tests/test_advanced/test_uuid/test_tutorial002.py b/tests/test_advanced/test_uuid/test_tutorial002.py index c9f4e5a35d..b1ff5ba3fd 100644 --- a/tests/test_advanced/test_uuid/test_tutorial002.py +++ b/tests/test_advanced/test_uuid/test_tutorial002.py @@ -1,31 +1,38 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from dirty_equals import IsUUID from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 -def test_tutorial() -> None: - from docs_src.advanced.uuid import tutorial002 as mod - +@pytest.fixture( + name="mod", + params=[ + "tutorial002", + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.advanced.uuid.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - first_uuid = calls[1][0]["id"] +def test_tutorial(print_mock: PrintMock, mod: ModuleType) -> None: + mod.main() + first_uuid = print_mock.calls[1][0]["id"] assert first_uuid == IsUUID(4) - second_uuid = calls[7][0]["id"] + second_uuid = print_mock.calls[7][0]["id"] assert second_uuid == IsUUID(4) assert first_uuid != second_uuid - assert calls == [ + assert print_mock.calls == [ ["The hero before saving in the DB"], [ { diff --git a/tests/test_advanced/test_uuid/test_tutorial002_py310.py b/tests/test_advanced/test_uuid/test_tutorial002_py310.py deleted file mode 100644 index ba472e30fd..0000000000 --- a/tests/test_advanced/test_uuid/test_tutorial002_py310.py +++ /dev/null @@ -1,72 +0,0 @@ -from unittest.mock import patch - -from dirty_equals import IsUUID -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial() -> None: - from docs_src.advanced.uuid import tutorial002_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - first_uuid = calls[1][0]["id"] - assert first_uuid == IsUUID(4) - - second_uuid = calls[7][0]["id"] - assert second_uuid == IsUUID(4) - - assert first_uuid != second_uuid - - assert calls == [ - ["The hero before saving in the DB"], - [ - { - "name": "Deadpond", - "secret_name": "Dive Wilson", - "id": first_uuid, - "age": None, - } - ], - ["The hero ID was already set"], - [first_uuid], - ["After saving in the DB"], - [ - { - "name": "Deadpond", - "secret_name": "Dive Wilson", - "age": None, - "id": first_uuid, - } - ], - ["Created hero:"], - [ - { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "age": None, - "id": second_uuid, - } - ], - ["Created hero ID:"], - [second_uuid], - ["Selected hero:"], - [ - { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "age": None, - "id": second_uuid, - } - ], - ["Selected hero ID:"], - [second_uuid], - ] diff --git a/tests/test_tutorial/test_code_structure/test_tutorial002.py b/tests/test_tutorial/test_code_structure/test_tutorial002.py index ccbb849097..f1d4043e85 100644 --- a/tests/test_tutorial/test_code_structure/test_tutorial002.py +++ b/tests/test_tutorial/test_code_structure/test_tutorial002.py @@ -1,8 +1,11 @@ -from unittest.mock import patch +import importlib +from dataclasses import dataclass +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py39, needs_py310 expected_calls = [ [ @@ -22,16 +25,34 @@ ] -def test_tutorial(): - from docs_src.tutorial.code_structure.tutorial002 import app, database +@dataclass +class Modules: + app: ModuleType + database: ModuleType - database.sqlite_url = "sqlite://" - database.engine = create_engine(database.sqlite_url) - app.engine = database.engine - calls = [] - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - app.main() - assert calls == expected_calls +@pytest.fixture( + name="modules", + params=[ + "tutorial002", + pytest.param("tutorial002_py39", marks=needs_py39), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_modules(request: pytest.FixtureRequest) -> Modules: + app_module = importlib.import_module( + f"docs_src.tutorial.code_structure.{request.param}.app" + ) + database_module = importlib.import_module( + f"docs_src.tutorial.code_structure.{request.param}.database" + ) + database_module.sqlite_url = "sqlite://" + database_module.engine = create_engine(database_module.sqlite_url) + app_module.engine = database_module.engine + + return Modules(app=app_module, database=database_module) + + +def test_tutorial(print_mock: PrintMock, modules: Modules): + modules.app.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_code_structure/test_tutorial002_py310.py b/tests/test_tutorial/test_code_structure/test_tutorial002_py310.py deleted file mode 100644 index be28486652..0000000000 --- a/tests/test_tutorial/test_code_structure/test_tutorial002_py310.py +++ /dev/null @@ -1,38 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Created hero:", - { - "id": 1, - "name": "Deadpond", - "age": None, - "secret_name": "Dive Wilson", - "team_id": 1, - }, - ], - [ - "Hero's team:", - {"name": "Z-Force", "headquarters": "Sister Margaret's Bar", "id": 1}, - ], -] - - -@needs_py310 -def test_tutorial(): - from docs_src.tutorial.code_structure.tutorial002_py310 import app, database - - database.sqlite_url = "sqlite://" - database.engine = create_engine(database.sqlite_url) - app.engine = database.engine - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - app.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_code_structure/test_tutorial002_py39.py b/tests/test_tutorial/test_code_structure/test_tutorial002_py39.py deleted file mode 100644 index 55f6a435dc..0000000000 --- a/tests/test_tutorial/test_code_structure/test_tutorial002_py39.py +++ /dev/null @@ -1,38 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py39 - -expected_calls = [ - [ - "Created hero:", - { - "id": 1, - "name": "Deadpond", - "age": None, - "secret_name": "Dive Wilson", - "team_id": 1, - }, - ], - [ - "Hero's team:", - {"name": "Z-Force", "headquarters": "Sister Margaret's Bar", "id": 1}, - ], -] - - -@needs_py39 -def test_tutorial(): - from docs_src.tutorial.code_structure.tutorial002_py39 import app, database - - database.sqlite_url = "sqlite://" - database.engine = create_engine(database.sqlite_url) - app.engine = database.engine - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - app.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_create_connected_tables/test_tutorial001.py b/tests/test_tutorial/test_connect/test_create_connected_tables/test_tutorial001.py index 265a05931c..49d4acfa00 100644 --- a/tests/test_tutorial/test_connect/test_create_connected_tables/test_tutorial001.py +++ b/tests/test_tutorial/test_connect/test_create_connected_tables/test_tutorial001.py @@ -1,14 +1,32 @@ +import importlib +from types import ModuleType + +import pytest from sqlalchemy import inspect from sqlalchemy.engine.reflection import Inspector from sqlmodel import create_engine +from tests.conftest import needs_py310 + + +@pytest.fixture( + name="module", + params=[ + "tutorial001", + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module( + f"docs_src.tutorial.connect.create_tables.{request.param}" + ) + return module -def test_tutorial001(): - from docs_src.tutorial.connect.create_tables import tutorial001 as mod - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - mod.main() - insp: Inspector = inspect(mod.engine) - assert insp.has_table(str(mod.Hero.__tablename__)) - assert insp.has_table(str(mod.Team.__tablename__)) +def test_tutorial001(module: ModuleType): + module.sqlite_url = "sqlite://" + module.engine = create_engine(module.sqlite_url) + module.main() + insp: Inspector = inspect(module.engine) + assert insp.has_table(str(module.Hero.__tablename__)) + assert insp.has_table(str(module.Team.__tablename__)) diff --git a/tests/test_tutorial/test_connect/test_create_connected_tables/test_tutorial001_py310.py b/tests/test_tutorial/test_connect/test_create_connected_tables/test_tutorial001_py310.py deleted file mode 100644 index 95f15a4266..0000000000 --- a/tests/test_tutorial/test_connect/test_create_connected_tables/test_tutorial001_py310.py +++ /dev/null @@ -1,17 +0,0 @@ -from sqlalchemy import inspect -from sqlalchemy.engine.reflection import Inspector -from sqlmodel import create_engine - -from ....conftest import needs_py310 - - -@needs_py310 -def test_tutorial001(): - from docs_src.tutorial.connect.create_tables import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - mod.main() - insp: Inspector = inspect(mod.engine) - assert insp.has_table(str(mod.Hero.__tablename__)) - assert insp.has_table(str(mod.Team.__tablename__)) diff --git a/tests/test_tutorial/test_connect/test_delete/test_tutorial001.py b/tests/test_tutorial/test_connect/test_delete/test_tutorial001.py index 1a9fe293ba..cc25dd42e3 100644 --- a/tests/test_tutorial/test_connect/test_delete/test_tutorial001.py +++ b/tests/test_tutorial/test_connect/test_delete/test_tutorial001.py @@ -1,8 +1,10 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ....conftest import get_testing_print_function +from ....conftest import PrintMock, needs_py310 expected_calls = [ [ @@ -58,15 +60,22 @@ ] -def test_tutorial(): - from docs_src.tutorial.connect.delete import tutorial001 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] +@pytest.fixture( + name="module", + params=[ + "tutorial001", + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module( + f"docs_src.tutorial.connect.delete.{request.param}" + ) + module.sqlite_url = "sqlite://" + module.engine = create_engine(module.sqlite_url) + return module - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, module: ModuleType): + module.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_delete/test_tutorial001_py310.py b/tests/test_tutorial/test_connect/test_delete/test_tutorial001_py310.py deleted file mode 100644 index f1bef3ed02..0000000000 --- a/tests/test_tutorial/test_connect/test_delete/test_tutorial001_py310.py +++ /dev/null @@ -1,73 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py310 - -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", - }, - ], -] - - -@needs_py310 -def test_tutorial(): - from docs_src.tutorial.connect.delete import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_insert/test_tutorial001.py b/tests/test_tutorial/test_connect/test_insert/test_tutorial001.py index cfc08ee854..6c4ec3ab1c 100644 --- a/tests/test_tutorial/test_connect/test_insert/test_tutorial001.py +++ b/tests/test_tutorial/test_connect/test_insert/test_tutorial001.py @@ -1,8 +1,10 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ....conftest import get_testing_print_function +from ....conftest import PrintMock, needs_py310 expected_calls = [ [ @@ -38,15 +40,22 @@ ] -def test_tutorial001(): - from docs_src.tutorial.connect.insert import tutorial001 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +@pytest.fixture( + name="module", + params=[ + "tutorial001", + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module( + f"docs_src.tutorial.connect.insert.{request.param}" + ) + module.sqlite_url = "sqlite://" + module.engine = create_engine(module.sqlite_url) + return module + + +def test_tutorial001(print_mock: PrintMock, module: ModuleType): + module.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_insert/test_tutorial001_py310.py b/tests/test_tutorial/test_connect/test_insert/test_tutorial001_py310.py deleted file mode 100644 index 6dabc10b80..0000000000 --- a/tests/test_tutorial/test_connect/test_insert/test_tutorial001_py310.py +++ /dev/null @@ -1,53 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py310 - -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", - }, - ], -] - - -@needs_py310 -def test_tutorial001(): - from docs_src.tutorial.connect.insert import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial001_py310_tutorial002_py310.py b/tests/test_tutorial/test_connect/test_select/test_tutorial001_py310_tutorial002_py310.py deleted file mode 100644 index 4809d79b68..0000000000 --- a/tests/test_tutorial/test_connect/test_select/test_tutorial001_py310_tutorial002_py310.py +++ /dev/null @@ -1,92 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py310 - -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"}, - ], -] - - -@needs_py310 -def test_tutorial001(): - from docs_src.tutorial.connect.select import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls - - -@needs_py310 -def test_tutorial002(): - from docs_src.tutorial.connect.select import tutorial002_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_connect/test_select/test_tutorial001_tutorial002.py index c0d6b59dd9..6553bc1ae6 100644 --- a/tests/test_tutorial/test_connect/test_select/test_tutorial001_tutorial002.py +++ b/tests/test_tutorial/test_connect/test_select/test_tutorial001_tutorial002.py @@ -1,8 +1,10 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ....conftest import get_testing_print_function +from ....conftest import PrintMock, needs_py310 expected_calls = [ [ @@ -62,29 +64,37 @@ ] -def test_tutorial001(): - from docs_src.tutorial.connect.select import tutorial001 as mod +@pytest.fixture(name="module") +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module( + f"docs_src.tutorial.connect.select.{request.param}" + ) + module.sqlite_url = "sqlite://" + module.engine = create_engine(module.sqlite_url) + return module - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls - - -def test_tutorial002(): - from docs_src.tutorial.connect.select import tutorial002 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] +@pytest.mark.parametrize( + "module", + [ + "tutorial001", + pytest.param("tutorial001_py310", marks=needs_py310), + ], + indirect=True, +) +def test_tutorial001(print_mock: PrintMock, module: ModuleType): + module.main() + assert print_mock.calls == expected_calls - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +@pytest.mark.parametrize( + "module", + [ + "tutorial002", + pytest.param("tutorial002_py310", marks=needs_py310), + ], + indirect=True, +) +def test_tutorial002(print_mock: PrintMock, module: ModuleType): + module.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial003.py b/tests/test_tutorial/test_connect/test_select/test_tutorial003.py index f309e1c44e..6036b672a3 100644 --- a/tests/test_tutorial/test_connect/test_select/test_tutorial003.py +++ b/tests/test_tutorial/test_connect/test_select/test_tutorial003.py @@ -1,8 +1,10 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ....conftest import get_testing_print_function +from ....conftest import PrintMock, needs_py310 expected_calls = [ [ @@ -74,15 +76,22 @@ ] -def test_tutorial(): - from docs_src.tutorial.connect.select import tutorial003 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] +@pytest.fixture( + name="module", + params=[ + "tutorial003", + pytest.param("tutorial003_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module( + f"docs_src.tutorial.connect.select.{request.param}" + ) + module.sqlite_url = "sqlite://" + module.engine = create_engine(module.sqlite_url) + return module - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, module: ModuleType): + module.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial003_py310.py b/tests/test_tutorial/test_connect/test_select/test_tutorial003_py310.py deleted file mode 100644 index e826ce44ae..0000000000 --- a/tests/test_tutorial/test_connect/test_select/test_tutorial003_py310.py +++ /dev/null @@ -1,89 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py310 - -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, - ], -] - - -@needs_py310 -def test_tutorial(): - from docs_src.tutorial.connect.select import tutorial003_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial004.py b/tests/test_tutorial/test_connect/test_select/test_tutorial004.py index a33c814856..c9fa695eda 100644 --- a/tests/test_tutorial/test_connect/test_select/test_tutorial004.py +++ b/tests/test_tutorial/test_connect/test_select/test_tutorial004.py @@ -1,8 +1,10 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ....conftest import get_testing_print_function +from ....conftest import PrintMock, needs_py310 expected_calls = [ [ @@ -48,15 +50,22 @@ ] -def test_tutorial(): - from docs_src.tutorial.connect.select import tutorial004 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] +@pytest.fixture( + name="module", + params=[ + "tutorial004", + pytest.param("tutorial004_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module( + f"docs_src.tutorial.connect.select.{request.param}" + ) + module.sqlite_url = "sqlite://" + module.engine = create_engine(module.sqlite_url) + return module - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, module: ModuleType): + module.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial004_py310.py b/tests/test_tutorial/test_connect/test_select/test_tutorial004_py310.py deleted file mode 100644 index 33dd8a4329..0000000000 --- a/tests/test_tutorial/test_connect/test_select/test_tutorial004_py310.py +++ /dev/null @@ -1,63 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py310 - -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", - }, - ], -] - - -@needs_py310 -def test_tutorial(): - from docs_src.tutorial.connect.select import tutorial004_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial005.py b/tests/test_tutorial/test_connect/test_select/test_tutorial005.py index f7ad78dc65..12d98b7e66 100644 --- a/tests/test_tutorial/test_connect/test_select/test_tutorial005.py +++ b/tests/test_tutorial/test_connect/test_select/test_tutorial005.py @@ -1,8 +1,10 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ....conftest import get_testing_print_function +from ....conftest import PrintMock, needs_py310 expected_calls = [ [ @@ -50,15 +52,22 @@ ] -def test_tutorial(): - from docs_src.tutorial.connect.select import tutorial005 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] +@pytest.fixture( + name="module", + params=[ + "tutorial005", + pytest.param("tutorial005_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module( + f"docs_src.tutorial.connect.select.{request.param}" + ) + module.sqlite_url = "sqlite://" + module.engine = create_engine(module.sqlite_url) + return module - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, module: ModuleType): + module.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial005_py310.py b/tests/test_tutorial/test_connect/test_select/test_tutorial005_py310.py deleted file mode 100644 index 8cddb6455a..0000000000 --- a/tests/test_tutorial/test_connect/test_select/test_tutorial005_py310.py +++ /dev/null @@ -1,65 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py310 - -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"}, - ], -] - - -@needs_py310 -def test_tutorial(): - from docs_src.tutorial.connect.select import tutorial005_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_update/test_tutorial001.py b/tests/test_tutorial/test_connect/test_update/test_tutorial001.py index d6875946c1..aa94c91216 100644 --- a/tests/test_tutorial/test_connect/test_update/test_tutorial001.py +++ b/tests/test_tutorial/test_connect/test_update/test_tutorial001.py @@ -1,8 +1,10 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ....conftest import get_testing_print_function +from ....conftest import PrintMock, needs_py310 expected_calls = [ [ @@ -48,15 +50,22 @@ ] -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.connect.update import tutorial001 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] +@pytest.fixture( + name="module", + params=[ + "tutorial001", + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module( + f"docs_src.tutorial.connect.update.{request.param}" + ) + module.sqlite_url = "sqlite://" + module.engine = create_engine(module.sqlite_url) + return module - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, module: ModuleType): + module.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_connect/test_update/test_tutorial001_py310.py b/tests/test_tutorial/test_connect/test_update/test_tutorial001_py310.py deleted file mode 100644 index f3702654c2..0000000000 --- a/tests/test_tutorial/test_connect/test_update/test_tutorial001_py310.py +++ /dev/null @@ -1,63 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py310 - -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", - }, - ], -] - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.connect.update import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_create_db_and_table/test_tutorial001.py b/tests/test_tutorial/test_create_db_and_table/test_tutorial001.py index b6a2e72628..4aeeb64b6c 100644 --- a/tests/test_tutorial/test_create_db_and_table/test_tutorial001.py +++ b/tests/test_tutorial/test_create_db_and_table/test_tutorial001.py @@ -1,10 +1,19 @@ from pathlib import Path -from ...conftest import coverage_run +import pytest +from ...conftest import coverage_run, needs_py310 -def test_create_db_and_table(cov_tmp_path: Path): - module = "docs_src.tutorial.create_db_and_table.tutorial001" + +@pytest.mark.parametrize( + "module_name", + [ + "tutorial001", + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def test_create_db_and_table(cov_tmp_path: Path, module_name: str): + module = f"docs_src.tutorial.create_db_and_table.{module_name}" result = coverage_run(module=module, cwd=cov_tmp_path) assert "BEGIN" in result.stdout assert 'PRAGMA main.table_info("hero")' in result.stdout diff --git a/tests/test_tutorial/test_create_db_and_table/test_tutorial001_py310.py b/tests/test_tutorial/test_create_db_and_table/test_tutorial001_py310.py deleted file mode 100644 index 465b9f9d58..0000000000 --- a/tests/test_tutorial/test_create_db_and_table/test_tutorial001_py310.py +++ /dev/null @@ -1,19 +0,0 @@ -from pathlib import Path - -from ...conftest import coverage_run, needs_py310 - - -@needs_py310 -def test_create_db_and_table(cov_tmp_path: Path): - module = "docs_src.tutorial.create_db_and_table.tutorial001_py310" - result = coverage_run(module=module, cwd=cov_tmp_path) - assert "BEGIN" in result.stdout - assert 'PRAGMA main.table_info("hero")' in result.stdout - assert "CREATE TABLE hero (" in result.stdout - assert "id INTEGER NOT NULL," in result.stdout - assert "name VARCHAR NOT NULL," in result.stdout - assert "secret_name VARCHAR NOT NULL," in result.stdout - assert "age INTEGER," in result.stdout - assert "PRIMARY KEY (id)" in result.stdout - assert ")" in result.stdout - assert "COMMIT" in result.stdout diff --git a/tests/test_tutorial/test_create_db_and_table/test_tutorial002.py b/tests/test_tutorial/test_create_db_and_table/test_tutorial002.py index 3a24ae1609..0f1ad85fe9 100644 --- a/tests/test_tutorial/test_create_db_and_table/test_tutorial002.py +++ b/tests/test_tutorial/test_create_db_and_table/test_tutorial002.py @@ -1,13 +1,31 @@ +import importlib +from types import ModuleType + +import pytest from sqlalchemy import inspect from sqlalchemy.engine.reflection import Inspector from sqlmodel import create_engine +from ...conftest import needs_py310 + + +@pytest.fixture( + name="module", + params=[ + "tutorial002", + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module( + f"docs_src.tutorial.create_db_and_table.{request.param}" + ) + module.sqlite_url = "sqlite://" + module.engine = create_engine(module.sqlite_url) + return module -def test_create_db_and_table(clear_sqlmodel): - from docs_src.tutorial.create_db_and_table import tutorial002 as mod - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - mod.create_db_and_tables() - insp: Inspector = inspect(mod.engine) - assert insp.has_table(str(mod.Hero.__tablename__)) +def test_create_db_and_table(module: ModuleType): + module.create_db_and_tables() + insp: Inspector = inspect(module.engine) + assert insp.has_table(str(module.Hero.__tablename__)) diff --git a/tests/test_tutorial/test_create_db_and_table/test_tutorial002_py310.py b/tests/test_tutorial/test_create_db_and_table/test_tutorial002_py310.py deleted file mode 100644 index 3ca3186b9e..0000000000 --- a/tests/test_tutorial/test_create_db_and_table/test_tutorial002_py310.py +++ /dev/null @@ -1,16 +0,0 @@ -from sqlalchemy import inspect -from sqlalchemy.engine.reflection import Inspector -from sqlmodel import create_engine - -from ...conftest import needs_py310 - - -@needs_py310 -def test_create_db_and_table(clear_sqlmodel): - from docs_src.tutorial.create_db_and_table import tutorial002_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - mod.create_db_and_tables() - insp: Inspector = inspect(mod.engine) - assert insp.has_table(str(mod.Hero.__tablename__)) diff --git a/tests/test_tutorial/test_create_db_and_table/test_tutorial003.py b/tests/test_tutorial/test_create_db_and_table/test_tutorial003.py index e5c55c70f3..446feae71a 100644 --- a/tests/test_tutorial/test_create_db_and_table/test_tutorial003.py +++ b/tests/test_tutorial/test_create_db_and_table/test_tutorial003.py @@ -1,13 +1,31 @@ +import importlib +from types import ModuleType + +import pytest from sqlalchemy import inspect from sqlalchemy.engine.reflection import Inspector from sqlmodel import create_engine +from ...conftest import needs_py310 + + +@pytest.fixture( + name="module", + params=[ + "tutorial003", + pytest.param("tutorial003_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module( + f"docs_src.tutorial.create_db_and_table.{request.param}" + ) + module.sqlite_url = "sqlite://" + module.engine = create_engine(module.sqlite_url) + return module -def test_create_db_and_table(clear_sqlmodel): - from docs_src.tutorial.create_db_and_table import tutorial003 as mod - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - mod.create_db_and_tables() - insp: Inspector = inspect(mod.engine) - assert insp.has_table(str(mod.Hero.__tablename__)) +def test_create_db_and_table(module: ModuleType): + module.create_db_and_tables() + insp: Inspector = inspect(module.engine) + assert insp.has_table(str(module.Hero.__tablename__)) diff --git a/tests/test_tutorial/test_create_db_and_table/test_tutorial003_py310.py b/tests/test_tutorial/test_create_db_and_table/test_tutorial003_py310.py deleted file mode 100644 index a1806ce250..0000000000 --- a/tests/test_tutorial/test_create_db_and_table/test_tutorial003_py310.py +++ /dev/null @@ -1,16 +0,0 @@ -from sqlalchemy import inspect -from sqlalchemy.engine.reflection import Inspector -from sqlmodel import create_engine - -from ...conftest import needs_py310 - - -@needs_py310 -def test_create_db_and_table(clear_sqlmodel): - from docs_src.tutorial.create_db_and_table import tutorial003_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - mod.create_db_and_tables() - insp: Inspector = inspect(mod.engine) - assert insp.has_table(str(mod.Hero.__tablename__)) diff --git a/tests/test_tutorial/test_delete/test_tutorial001_py310_tutorial002_py310.py b/tests/test_tutorial/test_delete/test_tutorial001_py310_tutorial002_py310.py deleted file mode 100644 index 0f97e7489f..0000000000 --- a/tests/test_tutorial/test_delete/test_tutorial001_py310_tutorial002_py310.py +++ /dev/null @@ -1,88 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Hero 1:", - {"id": 2, "name": "Spider-Boy", "secret_name": "Pedro Parqueador", "age": None}, - ], - [ - "Hero 2:", - { - "id": 7, - "name": "Captain North America", - "secret_name": "Esteban Rogelios", - "age": 93, - }, - ], - [ - "Updated hero 1:", - { - "id": 2, - "name": "Spider-Youngster", - "secret_name": "Pedro Parqueador", - "age": 16, - }, - ], - [ - "Updated hero 2:", - { - "id": 7, - "name": "Captain North America Except Canada", - "secret_name": "Esteban Rogelios", - "age": 110, - }, - ], - [ - "Hero: ", - { - "id": 2, - "name": "Spider-Youngster", - "secret_name": "Pedro Parqueador", - "age": 16, - }, - ], - [ - "Deleted hero:", - { - "id": 2, - "name": "Spider-Youngster", - "secret_name": "Pedro Parqueador", - "age": 16, - }, - ], - ["There's no hero named Spider-Youngster"], -] - - -@needs_py310 -def test_tutorial001(clear_sqlmodel): - from docs_src.tutorial.delete import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls - - -@needs_py310 -def test_tutorial002(clear_sqlmodel): - from docs_src.tutorial.delete import tutorial002_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_delete/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_delete/test_tutorial001_tutorial002.py index 1d6497b327..3d755ae027 100644 --- a/tests/test_tutorial/test_delete/test_tutorial001_tutorial002.py +++ b/tests/test_tutorial/test_delete/test_tutorial001_tutorial002.py @@ -1,8 +1,10 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 expected_calls = [ [ @@ -58,29 +60,35 @@ ] -def test_tutorial001(clear_sqlmodel): - from docs_src.tutorial.delete import tutorial001 as mod +@pytest.fixture(name="module") +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module(f"docs_src.tutorial.delete.{request.param}") + module.sqlite_url = "sqlite://" + module.engine = create_engine(module.sqlite_url) + return module - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls - - -def test_tutorial002(clear_sqlmodel): - from docs_src.tutorial.delete import tutorial002 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] +@pytest.mark.parametrize( + "module", + [ + "tutorial001", + pytest.param("tutorial001_py310", marks=needs_py310), + ], + indirect=True, +) +def test_tutorial001(print_mock: PrintMock, module: ModuleType): + module.main() + assert print_mock.calls == expected_calls - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +@pytest.mark.parametrize( + "module", + [ + "tutorial002", + pytest.param("tutorial002_py310", marks=needs_py310), + ], + indirect=True, +) +def test_tutorial002(print_mock: PrintMock, module: ModuleType): + module.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_py310_tests_main.py b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_py310_tests_main.py deleted file mode 100644 index 781de7c772..0000000000 --- a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_py310_tests_main.py +++ /dev/null @@ -1,25 +0,0 @@ -import subprocess -from pathlib import Path - -from ....conftest import needs_py310 - - -@needs_py310 -def test_run_tests(clear_sqlmodel): - from docs_src.tutorial.fastapi.app_testing.tutorial001_py310 import test_main as mod - - test_path = Path(mod.__file__).resolve().parent - top_level_path = Path(__file__).resolve().parent.parent.parent.parent.parent - result = subprocess.run( - [ - "coverage", - "run", - "--parallel-mode", - "-m", - "pytest", - test_path, - ], - cwd=top_level_path, - capture_output=True, - ) - assert result.returncode == 0, result.stdout.decode("utf-8") diff --git a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_py39_tests_main.py b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_py39_tests_main.py deleted file mode 100644 index 6dbcc80d56..0000000000 --- a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_py39_tests_main.py +++ /dev/null @@ -1,25 +0,0 @@ -import subprocess -from pathlib import Path - -from ....conftest import needs_py39 - - -@needs_py39 -def test_run_tests(clear_sqlmodel): - from docs_src.tutorial.fastapi.app_testing.tutorial001_py39 import test_main as mod - - test_path = Path(mod.__file__).resolve().parent - top_level_path = Path(__file__).resolve().parent.parent.parent.parent.parent - result = subprocess.run( - [ - "coverage", - "run", - "--parallel-mode", - "-m", - "pytest", - test_path, - ], - cwd=top_level_path, - capture_output=True, - ) - assert result.returncode == 0, result.stdout.decode("utf-8") diff --git a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests001.py b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests001.py index 4da11c2121..95be986fc9 100644 --- a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests001.py +++ b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests001.py @@ -1,18 +1,44 @@ import importlib +import sys +from dataclasses import dataclass +from types import ModuleType import pytest -from docs_src.tutorial.fastapi.app_testing.tutorial001 import main as app_mod -from docs_src.tutorial.fastapi.app_testing.tutorial001 import test_main_001 as test_mod +from tests.conftest import needs_py39, needs_py310 -@pytest.fixture(name="prepare", autouse=True) -def prepare_fixture(clear_sqlmodel): +@dataclass +class Modules: + app: ModuleType + test: ModuleType + + +@pytest.fixture( + name="modules_path", + params=[ + "tutorial001", + pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_modules_path(request: pytest.FixtureRequest) -> str: + return f"docs_src.tutorial.fastapi.app_testing.{request.param}" + + +@pytest.fixture(name="modules") +def load_modules(clear_sqlmodel, modules_path: str) -> Modules: # Trigger side effects of registering table models in SQLModel # This has to be called after clear_sqlmodel - importlib.reload(app_mod) - importlib.reload(test_mod) + app_mod_path = f"{modules_path}.main" + if app_mod_path in sys.modules: + app_mod = sys.modules[app_mod_path] + importlib.reload(app_mod) + else: + app_mod = importlib.import_module(app_mod_path) + test_mod = importlib.import_module(f"{modules_path}.test_main_001") + return Modules(app=app_mod, test=test_mod) -def test_tutorial(): - test_mod.test_create_hero() +def test_tutorial(modules: Modules): + modules.test.test_create_hero() diff --git a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests002.py b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests002.py index 241e92323b..1f360c6f12 100644 --- a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests002.py +++ b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests002.py @@ -1,18 +1,44 @@ import importlib +import sys +from dataclasses import dataclass +from types import ModuleType import pytest -from docs_src.tutorial.fastapi.app_testing.tutorial001 import main as app_mod -from docs_src.tutorial.fastapi.app_testing.tutorial001 import test_main_002 as test_mod +from tests.conftest import needs_py39, needs_py310 -@pytest.fixture(name="prepare", autouse=True) -def prepare_fixture(clear_sqlmodel): +@dataclass +class Modules: + app: ModuleType + test: ModuleType + + +@pytest.fixture( + name="modules_path", + params=[ + "tutorial001", + pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_modules_path(request: pytest.FixtureRequest) -> str: + return f"docs_src.tutorial.fastapi.app_testing.{request.param}" + + +@pytest.fixture(name="modules") +def load_modules(clear_sqlmodel, modules_path: str) -> Modules: # Trigger side effects of registering table models in SQLModel # This has to be called after clear_sqlmodel - importlib.reload(app_mod) - importlib.reload(test_mod) + app_mod_path = f"{modules_path}.main" + if app_mod_path in sys.modules: + app_mod = sys.modules[app_mod_path] + importlib.reload(app_mod) + else: + app_mod = importlib.import_module(app_mod_path) + test_mod = importlib.import_module(f"{modules_path}.test_main_002") + return Modules(app=app_mod, test=test_mod) -def test_tutorial(): - test_mod.test_create_hero() +def test_tutorial(modules: Modules): + modules.test.test_create_hero() diff --git a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests003.py b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests003.py index 32e0161bad..4911173b62 100644 --- a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests003.py +++ b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests003.py @@ -1,18 +1,44 @@ import importlib +import sys +from dataclasses import dataclass +from types import ModuleType import pytest -from docs_src.tutorial.fastapi.app_testing.tutorial001 import main as app_mod -from docs_src.tutorial.fastapi.app_testing.tutorial001 import test_main_003 as test_mod +from tests.conftest import needs_py39, needs_py310 -@pytest.fixture(name="prepare", autouse=True) -def prepare_fixture(clear_sqlmodel): +@dataclass +class Modules: + app: ModuleType + test: ModuleType + + +@pytest.fixture( + name="modules_path", + params=[ + "tutorial001", + pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_modules_path(request: pytest.FixtureRequest) -> str: + return f"docs_src.tutorial.fastapi.app_testing.{request.param}" + + +@pytest.fixture(name="modules") +def load_modules(clear_sqlmodel, modules_path: str) -> Modules: # Trigger side effects of registering table models in SQLModel # This has to be called after clear_sqlmodel - importlib.reload(app_mod) - importlib.reload(test_mod) + app_mod_path = f"{modules_path}.main" + if app_mod_path in sys.modules: + app_mod = sys.modules[app_mod_path] + importlib.reload(app_mod) + else: + app_mod = importlib.import_module(app_mod_path) + test_mod = importlib.import_module(f"{modules_path}.test_main_003") + return Modules(app=app_mod, test=test_mod) -def test_tutorial(): - test_mod.test_create_hero() +def test_tutorial(modules: Modules): + modules.test.test_create_hero() diff --git a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests004.py b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests004.py index c6402b2429..b3ca441d24 100644 --- a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests004.py +++ b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests004.py @@ -1,18 +1,44 @@ import importlib +import sys +from dataclasses import dataclass +from types import ModuleType import pytest -from docs_src.tutorial.fastapi.app_testing.tutorial001 import main as app_mod -from docs_src.tutorial.fastapi.app_testing.tutorial001 import test_main_004 as test_mod +from tests.conftest import needs_py39, needs_py310 -@pytest.fixture(name="prepare", autouse=True) -def prepare_fixture(clear_sqlmodel): +@dataclass +class Modules: + app: ModuleType + test: ModuleType + + +@pytest.fixture( + name="modules_path", + params=[ + "tutorial001", + pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_modules_path(request: pytest.FixtureRequest) -> str: + return f"docs_src.tutorial.fastapi.app_testing.{request.param}" + + +@pytest.fixture(name="modules") +def load_modules(clear_sqlmodel, modules_path: str) -> Modules: # Trigger side effects of registering table models in SQLModel # This has to be called after clear_sqlmodel - importlib.reload(app_mod) - importlib.reload(test_mod) + app_mod_path = f"{modules_path}.main" + if app_mod_path in sys.modules: + app_mod = sys.modules[app_mod_path] + importlib.reload(app_mod) + else: + app_mod = importlib.import_module(app_mod_path) + test_mod = importlib.import_module(f"{modules_path}.test_main_004") + return Modules(app=app_mod, test=test_mod) -def test_tutorial(): - test_mod.test_create_hero() +def test_tutorial(modules: Modules): + modules.test.test_create_hero() diff --git a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests_main.py b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests_main.py index d7c1329b38..28959f79d8 100644 --- a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests_main.py +++ b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests_main.py @@ -1,11 +1,30 @@ +import importlib import subprocess from pathlib import Path +from types import ModuleType +import pytest -def test_run_tests(clear_sqlmodel): - from docs_src.tutorial.fastapi.app_testing.tutorial001 import test_main as mod +from ....conftest import needs_py39, needs_py310 - test_path = Path(mod.__file__).resolve().parent + +@pytest.fixture( + name="module", + params=[ + "tutorial001", + pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module( + f"docs_src.tutorial.fastapi.app_testing.{request.param}.test_main" + ) + return module + + +def test_run_tests(module: ModuleType): + test_path = Path(module.__file__).resolve().parent top_level_path = Path(__file__).resolve().parent.parent.parent.parent.parent result = subprocess.run( [ diff --git a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py index f293199b40..1480a06f54 100644 --- a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py @@ -1,18 +1,34 @@ +import importlib +from types import ModuleType + +import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool +from tests.conftest import needs_py39, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.delete import tutorial001 as mod +@pytest.fixture( + name="module", + params=[ + "tutorial001", + pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.fastapi.delete.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine( mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool ) + return mod + - with TestClient(mod.app) as client: +def test_tutorial(module: ModuleType): + with TestClient(module.app) as client: hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} hero2_data = { "name": "Spider-Boy", diff --git a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py310.py deleted file mode 100644 index 2757c878bc..0000000000 --- a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py310.py +++ /dev/null @@ -1,377 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.delete import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - hero3_data = { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - } - response = client.post("/heroes/", json=hero1_data) - assert response.status_code == 200, response.text - response = client.post("/heroes/", json=hero2_data) - assert response.status_code == 200, response.text - hero2 = response.json() - hero2_id = hero2["id"] - response = client.post("/heroes/", json=hero3_data) - assert response.status_code == 200, response.text - response = client.get(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/9000") - assert response.status_code == 404, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 3 - response = client.patch( - f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} - ) - assert response.status_code == 200, response.text - response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) - assert response.status_code == 404, response.text - - response = client.delete(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 2 - - response = client.delete("/heroes/9000") - assert response.status_code == 404, response.text - - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100.0, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "delete": { - "summary": "Delete Hero", - "operationId": "delete_hero_heroes__hero_id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Hero", - "operationId": "update_hero_heroes__hero_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroUpdate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "HeroUpdate": { - "title": "HeroUpdate", - "type": "object", - "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "title": "Secret Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py39.py deleted file mode 100644 index 3299086bd0..0000000000 --- a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001_py39.py +++ /dev/null @@ -1,377 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.delete import tutorial001_py39 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - hero3_data = { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - } - response = client.post("/heroes/", json=hero1_data) - assert response.status_code == 200, response.text - response = client.post("/heroes/", json=hero2_data) - assert response.status_code == 200, response.text - hero2 = response.json() - hero2_id = hero2["id"] - response = client.post("/heroes/", json=hero3_data) - assert response.status_code == 200, response.text - response = client.get(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/9000") - assert response.status_code == 404, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 3 - response = client.patch( - f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} - ) - assert response.status_code == 200, response.text - response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) - assert response.status_code == 404, response.text - - response = client.delete(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 2 - - response = client.delete("/heroes/9000") - assert response.status_code == 404, response.text - - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100.0, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "delete": { - "summary": "Delete Hero", - "operationId": "delete_hero_heroes__hero_id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Hero", - "operationId": "update_hero_heroes__hero_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroUpdate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "HeroUpdate": { - "title": "HeroUpdate", - "type": "object", - "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "title": "Secret Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py index 4047539f0a..dc3db8b5b0 100644 --- a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py @@ -1,18 +1,36 @@ +import importlib +from types import ModuleType + +import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool +from tests.conftest import needs_py39, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.limit_and_offset import tutorial001 as mod +@pytest.fixture( + name="module", + params=[ + "tutorial001", + pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.fastapi.limit_and_offset.{request.param}" + ) mod.sqlite_url = "sqlite://" mod.engine = create_engine( mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool ) + return mod + - with TestClient(mod.app) as client: +def test_tutorial(module: ModuleType): + with TestClient(module.app) as client: hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} hero2_data = { "name": "Spider-Boy", diff --git a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py310.py deleted file mode 100644 index 480b92a121..0000000000 --- a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py310.py +++ /dev/null @@ -1,274 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.limit_and_offset import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - hero3_data = { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - } - response = client.post("/heroes/", json=hero1_data) - assert response.status_code == 200, response.text - response = client.post("/heroes/", json=hero2_data) - assert response.status_code == 200, response.text - hero2 = response.json() - hero_id = hero2["id"] - response = client.post("/heroes/", json=hero3_data) - assert response.status_code == 200, response.text - response = client.get(f"/heroes/{hero_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/9000") - assert response.status_code == 404, response.text - - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 3 - - response = client.get("/heroes/", params={"limit": 2}) - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 2 - assert data[0]["name"] == hero1_data["name"] - assert data[1]["name"] == hero2_data["name"] - - response = client.get("/heroes/", params={"offset": 1}) - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 2 - assert data[0]["name"] == hero2_data["name"] - assert data[1]["name"] == hero3_data["name"] - - response = client.get("/heroes/", params={"offset": 1, "limit": 1}) - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 1 - assert data[0]["name"] == hero2_data["name"] - - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100.0, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py39.py deleted file mode 100644 index 0a9d5c9ef0..0000000000 --- a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001_py39.py +++ /dev/null @@ -1,274 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.limit_and_offset import tutorial001_py39 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - hero3_data = { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - } - response = client.post("/heroes/", json=hero1_data) - assert response.status_code == 200, response.text - response = client.post("/heroes/", json=hero2_data) - assert response.status_code == 200, response.text - hero2 = response.json() - hero_id = hero2["id"] - response = client.post("/heroes/", json=hero3_data) - assert response.status_code == 200, response.text - response = client.get(f"/heroes/{hero_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/9000") - assert response.status_code == 404, response.text - - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 3 - - response = client.get("/heroes/", params={"limit": 2}) - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 2 - assert data[0]["name"] == hero1_data["name"] - assert data[1]["name"] == hero2_data["name"] - - response = client.get("/heroes/", params={"offset": 1}) - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 2 - assert data[0]["name"] == hero2_data["name"] - assert data[1]["name"] == hero3_data["name"] - - response = client.get("/heroes/", params={"offset": 1, "limit": 1}) - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 1 - assert data[0]["name"] == hero2_data["name"] - - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100.0, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py index 276a021c54..27629c891d 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py @@ -1,3 +1,7 @@ +import importlib +from types import ModuleType + +import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlalchemy import inspect @@ -5,16 +9,30 @@ from sqlmodel import create_engine from sqlmodel.pool import StaticPool +from tests.conftest import needs_py39, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.multiple_models import tutorial001 as mod +@pytest.fixture( + name="module", + params=[ + "tutorial001", + pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.fastapi.multiple_models.{request.param}" + ) mod.sqlite_url = "sqlite://" mod.engine = create_engine( mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool ) + return mod + - with TestClient(mod.app) as client: +def test_tutorial(module: ModuleType): + with TestClient(module.app) as client: hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} hero2_data = { "name": "Spider-Boy", @@ -195,8 +213,8 @@ def test_tutorial(clear_sqlmodel): } # Test inherited indexes - insp: Inspector = inspect(mod.engine) - indexes = insp.get_indexes(str(mod.Hero.__tablename__)) + insp: Inspector = inspect(module.engine) + indexes = insp.get_indexes(str(module.Hero.__tablename__)) expected_indexes = [ { "name": "ix_hero_name", diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py310.py deleted file mode 100644 index b6f082a0f8..0000000000 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py310.py +++ /dev/null @@ -1,220 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlalchemy import inspect -from sqlalchemy.engine.reflection import Inspector -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.multiple_models import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - response = client.post("/heroes/", json=hero1_data) - data = response.json() - - assert response.status_code == 200, response.text - assert data["name"] == hero1_data["name"] - assert data["secret_name"] == hero1_data["secret_name"] - assert data["id"] is not None - assert data["age"] is None - - response = client.post("/heroes/", json=hero2_data) - data = response.json() - - assert response.status_code == 200, response.text - assert data["name"] == hero2_data["name"] - assert data["secret_name"] == hero2_data["secret_name"] - assert data["id"] != hero2_data["id"], ( - "Now it's not possible to predefine the ID from the request, " - "it's now set by the database" - ) - assert data["age"] is None - - response = client.get("/heroes/") - data = response.json() - - assert response.status_code == 200, response.text - assert len(data) == 2 - assert data[0]["name"] == hero1_data["name"] - assert data[0]["secret_name"] == hero1_data["secret_name"] - assert data[1]["name"] == hero2_data["name"] - assert data[1]["secret_name"] == hero2_data["secret_name"] - assert data[1]["id"] != hero2_data["id"] - - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - } - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["id", "name", "secret_name"], - "type": "object", - "properties": { - "id": {"title": "Id", "type": "integer"}, - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } - - # Test inherited indexes - insp: Inspector = inspect(mod.engine) - indexes = insp.get_indexes(str(mod.Hero.__tablename__)) - expected_indexes = [ - { - "name": "ix_hero_name", - "dialect_options": {}, - "column_names": ["name"], - "unique": 0, - }, - { - "name": "ix_hero_age", - "dialect_options": {}, - "column_names": ["age"], - "unique": 0, - }, - ] - for index in expected_indexes: - assert index in indexes, "This expected index should be in the indexes in DB" - # Now that this index was checked, remove it from the list of indexes - indexes.pop(indexes.index(index)) - assert len(indexes) == 0, "The database should only have the expected indexes" diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py39.py deleted file mode 100644 index 82365ced61..0000000000 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001_py39.py +++ /dev/null @@ -1,221 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlalchemy import inspect -from sqlalchemy.engine.reflection import Inspector -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.multiple_models import tutorial001_py39 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - response = client.post("/heroes/", json=hero1_data) - data = response.json() - - assert response.status_code == 200, response.text - assert data["name"] == hero1_data["name"] - assert data["secret_name"] == hero1_data["secret_name"] - assert data["id"] is not None - assert data["age"] is None - - response = client.post("/heroes/", json=hero2_data) - data = response.json() - - assert response.status_code == 200, response.text - assert data["name"] == hero2_data["name"] - assert data["secret_name"] == hero2_data["secret_name"] - assert data["id"] != hero2_data["id"], ( - "Now it's not possible to predefine the ID from the request, " - "it's now set by the database" - ) - assert data["age"] is None - - response = client.get("/heroes/") - data = response.json() - - assert response.status_code == 200, response.text - assert len(data) == 2 - assert data[0]["name"] == hero1_data["name"] - assert data[0]["secret_name"] == hero1_data["secret_name"] - assert data[1]["name"] == hero2_data["name"] - assert data[1]["secret_name"] == hero2_data["secret_name"] - assert data[1]["id"] != hero2_data["id"] - - response = client.get("/openapi.json") - - assert response.status_code == 200, response.text - - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - } - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["id", "name", "secret_name"], - "type": "object", - "properties": { - "id": {"title": "Id", "type": "integer"}, - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } - - # Test inherited indexes - insp: Inspector = inspect(mod.engine) - indexes = insp.get_indexes(str(mod.Hero.__tablename__)) - expected_indexes = [ - { - "name": "ix_hero_name", - "dialect_options": {}, - "column_names": ["name"], - "unique": 0, - }, - { - "name": "ix_hero_age", - "dialect_options": {}, - "column_names": ["age"], - "unique": 0, - }, - ] - for index in expected_indexes: - assert index in indexes, "This expected index should be in the indexes in DB" - # Now that this index was checked, remove it from the list of indexes - indexes.pop(indexes.index(index)) - assert len(indexes) == 0, "The database should only have the expected indexes" diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py index 8327c6d566..c840bde253 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py @@ -1,3 +1,7 @@ +import importlib +from types import ModuleType + +import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlalchemy import inspect @@ -5,16 +9,30 @@ from sqlmodel import create_engine from sqlmodel.pool import StaticPool +from tests.conftest import needs_py39, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.multiple_models import tutorial002 as mod +@pytest.fixture( + name="module", + params=[ + "tutorial002", + pytest.param("tutorial002_py39", marks=needs_py39), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.fastapi.multiple_models.{request.param}" + ) mod.sqlite_url = "sqlite://" mod.engine = create_engine( mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool ) + return mod + - with TestClient(mod.app) as client: +def test_tutorial(module: ModuleType): + with TestClient(module.app) as client: hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} hero2_data = { "name": "Spider-Boy", @@ -195,8 +213,8 @@ def test_tutorial(clear_sqlmodel): } # Test inherited indexes - insp: Inspector = inspect(mod.engine) - indexes = insp.get_indexes(str(mod.Hero.__tablename__)) + insp: Inspector = inspect(module.engine) + indexes = insp.get_indexes(str(module.Hero.__tablename__)) expected_indexes = [ { "name": "ix_hero_age", diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py310.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py310.py deleted file mode 100644 index 30edc4dea3..0000000000 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py310.py +++ /dev/null @@ -1,221 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlalchemy import inspect -from sqlalchemy.engine.reflection import Inspector -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.multiple_models import tutorial002_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - response = client.post("/heroes/", json=hero1_data) - data = response.json() - - assert response.status_code == 200, response.text - assert data["name"] == hero1_data["name"] - assert data["secret_name"] == hero1_data["secret_name"] - assert data["id"] is not None - assert data["age"] is None - - response = client.post("/heroes/", json=hero2_data) - data = response.json() - - assert response.status_code == 200, response.text - assert data["name"] == hero2_data["name"] - assert data["secret_name"] == hero2_data["secret_name"] - assert data["id"] != hero2_data["id"], ( - "Now it's not possible to predefine the ID from the request, " - "it's now set by the database" - ) - assert data["age"] is None - - response = client.get("/heroes/") - data = response.json() - - assert response.status_code == 200, response.text - assert len(data) == 2 - assert data[0]["name"] == hero1_data["name"] - assert data[0]["secret_name"] == hero1_data["secret_name"] - assert data[1]["name"] == hero2_data["name"] - assert data[1]["secret_name"] == hero2_data["secret_name"] - assert data[1]["id"] != hero2_data["id"] - - response = client.get("/openapi.json") - - assert response.status_code == 200, response.text - - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - } - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } - - # Test inherited indexes - insp: Inspector = inspect(mod.engine) - indexes = insp.get_indexes(str(mod.Hero.__tablename__)) - expected_indexes = [ - { - "name": "ix_hero_age", - "dialect_options": {}, - "column_names": ["age"], - "unique": 0, - }, - { - "name": "ix_hero_name", - "dialect_options": {}, - "column_names": ["name"], - "unique": 0, - }, - ] - for index in expected_indexes: - assert index in indexes, "This expected index should be in the indexes in DB" - # Now that this index was checked, remove it from the list of indexes - indexes.pop(indexes.index(index)) - assert len(indexes) == 0, "The database should only have the expected indexes" diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py39.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py39.py deleted file mode 100644 index 2b86d3facc..0000000000 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002_py39.py +++ /dev/null @@ -1,221 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlalchemy import inspect -from sqlalchemy.engine.reflection import Inspector -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.multiple_models import tutorial002_py39 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - response = client.post("/heroes/", json=hero1_data) - data = response.json() - - assert response.status_code == 200, response.text - assert data["name"] == hero1_data["name"] - assert data["secret_name"] == hero1_data["secret_name"] - assert data["id"] is not None - assert data["age"] is None - - response = client.post("/heroes/", json=hero2_data) - data = response.json() - - assert response.status_code == 200, response.text - assert data["name"] == hero2_data["name"] - assert data["secret_name"] == hero2_data["secret_name"] - assert data["id"] != hero2_data["id"], ( - "Now it's not possible to predefine the ID from the request, " - "it's now set by the database" - ) - assert data["age"] is None - - response = client.get("/heroes/") - data = response.json() - - assert response.status_code == 200, response.text - assert len(data) == 2 - assert data[0]["name"] == hero1_data["name"] - assert data[0]["secret_name"] == hero1_data["secret_name"] - assert data[1]["name"] == hero2_data["name"] - assert data[1]["secret_name"] == hero2_data["secret_name"] - assert data[1]["id"] != hero2_data["id"] - - response = client.get("/openapi.json") - - assert response.status_code == 200, response.text - - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - } - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } - - # Test inherited indexes - insp: Inspector = inspect(mod.engine) - indexes = insp.get_indexes(str(mod.Hero.__tablename__)) - expected_indexes = [ - { - "name": "ix_hero_age", - "dialect_options": {}, - "column_names": ["age"], - "unique": 0, - }, - { - "name": "ix_hero_name", - "dialect_options": {}, - "column_names": ["name"], - "unique": 0, - }, - ] - for index in expected_indexes: - assert index in indexes, "This expected index should be in the indexes in DB" - # Now that this index was checked, remove it from the list of indexes - indexes.pop(indexes.index(index)) - assert len(indexes) == 0, "The database should only have the expected indexes" diff --git a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py index 9b1d527565..affea513dd 100644 --- a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py @@ -1,18 +1,34 @@ +import importlib +from types import ModuleType + +import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool +from tests.conftest import needs_py39, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.read_one import tutorial001 as mod +@pytest.fixture( + name="module", + params=[ + "tutorial001", + pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.fastapi.read_one.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine( mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool ) + return mod + - with TestClient(mod.app) as client: +def test_tutorial(module: ModuleType): + with TestClient(module.app) as client: hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} hero2_data = { "name": "Spider-Boy", diff --git a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py310.py deleted file mode 100644 index f18b0d65cf..0000000000 --- a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py310.py +++ /dev/null @@ -1,219 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.read_one import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - response = client.post("/heroes/", json=hero1_data) - assert response.status_code == 200, response.text - response = client.post("/heroes/", json=hero2_data) - assert response.status_code == 200, response.text - hero2 = response.json() - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 2 - - hero_id = hero2["id"] - response = client.get(f"/heroes/{hero_id}") - assert response.status_code == 200, response.text - data = response.json() - assert data == hero2 - - response = client.get("/heroes/9000") - assert response.status_code == 404, response.text - - response = client.get("/openapi.json") - - assert response.status_code == 200, response.text - - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - } - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py39.py deleted file mode 100644 index 4423d1a713..0000000000 --- a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001_py39.py +++ /dev/null @@ -1,219 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.read_one import tutorial001_py39 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - response = client.post("/heroes/", json=hero1_data) - assert response.status_code == 200, response.text - response = client.post("/heroes/", json=hero2_data) - assert response.status_code == 200, response.text - hero2 = response.json() - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 2 - - hero_id = hero2["id"] - response = client.get(f"/heroes/{hero_id}") - assert response.status_code == 200, response.text - data = response.json() - assert data == hero2 - - response = client.get("/heroes/9000") - assert response.status_code == 404, response.text - - response = client.get("/openapi.json") - - assert response.status_code == 200, response.text - - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - } - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py index 4b4f47b762..1d0446ed41 100644 --- a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py @@ -1,18 +1,36 @@ +import importlib +from types import ModuleType + +import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool +from tests.conftest import needs_py39, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.relationships import tutorial001 as mod +@pytest.fixture( + name="module", + params=[ + "tutorial001", + pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.fastapi.relationships.{request.param}" + ) mod.sqlite_url = "sqlite://" mod.engine = create_engine( mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool ) + return mod + - with TestClient(mod.app) as client: +def test_tutorial(module: ModuleType): + with TestClient(module.app) as client: team_preventers = {"name": "Preventers", "headquarters": "Sharp Tower"} team_z_force = {"name": "Z-Force", "headquarters": "Sister Margaret's Bar"} response = client.post("/teams/", json=team_preventers) diff --git a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py310.py deleted file mode 100644 index dcb78f597d..0000000000 --- a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py310.py +++ /dev/null @@ -1,767 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.relationships import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - team_preventers = {"name": "Preventers", "headquarters": "Sharp Tower"} - team_z_force = {"name": "Z-Force", "headquarters": "Sister Margaret's Bar"} - response = client.post("/teams/", json=team_preventers) - assert response.status_code == 200, response.text - team_preventers_data = response.json() - team_preventers_id = team_preventers_data["id"] - response = client.post("/teams/", json=team_z_force) - assert response.status_code == 200, response.text - team_z_force_data = response.json() - team_z_force_id = team_z_force_data["id"] - response = client.get("/teams/") - data = response.json() - assert len(data) == 2 - response = client.get("/teams/9000") - assert response.status_code == 404, response.text - response = client.patch( - f"/teams/{team_preventers_id}", json={"headquarters": "Preventers Tower"} - ) - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == team_preventers["name"] - assert data["headquarters"] == "Preventers Tower" - response = client.patch("/teams/9000", json={"name": "Freedom League"}) - assert response.status_code == 404, response.text - - hero1_data = { - "name": "Deadpond", - "secret_name": "Dive Wilson", - "team_id": team_z_force_id, - } - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - hero3_data = { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - "team_id": team_preventers_id, - } - response = client.post("/heroes/", json=hero1_data) - assert response.status_code == 200, response.text - hero1 = response.json() - hero1_id = hero1["id"] - response = client.post("/heroes/", json=hero2_data) - assert response.status_code == 200, response.text - hero2 = response.json() - hero2_id = hero2["id"] - response = client.post("/heroes/", json=hero3_data) - assert response.status_code == 200, response.text - response = client.get("/heroes/9000") - assert response.status_code == 404, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 3 - response = client.get(f"/heroes/{hero1_id}") - assert response.status_code == 200, response.text - data = response.json() - assert data["name"] == hero1_data["name"] - assert data["team"]["name"] == team_z_force["name"] - response = client.patch( - f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} - ) - assert response.status_code == 200, response.text - response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) - assert response.status_code == 404, response.text - response = client.delete(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 2 - response = client.delete("/heroes/9000") - assert response.status_code == 404, response.text - - response = client.get(f"/teams/{team_preventers_id}") - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == team_preventers_data["name"] - assert data["heroes"][0]["name"] == hero3_data["name"] - - response = client.delete(f"/teams/{team_preventers_id}") - assert response.status_code == 200, response.text - response = client.delete("/teams/9000") - assert response.status_code == 404, response.text - response = client.get("/teams/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 1 - - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100.0, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublicWithTeam" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "delete": { - "summary": "Delete Hero", - "operationId": "delete_hero_heroes__hero_id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Hero", - "operationId": "update_hero_heroes__hero_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroUpdate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/teams/": { - "get": { - "summary": "Read Teams", - "operationId": "read_teams_teams__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100.0, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Teams Teams Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/TeamPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Team", - "operationId": "create_team_teams__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/teams/{team_id}": { - "get": { - "summary": "Read Team", - "operationId": "read_team_teams__team_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Team Id", "type": "integer"}, - "name": "team_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamPublicWithHeroes" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "delete": { - "summary": "Delete Team", - "operationId": "delete_team_teams__team_id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Team Id", "type": "integer"}, - "name": "team_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Team", - "operationId": "update_team_teams__team_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Team Id", "type": "integer"}, - "name": "team_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamUpdate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "HeroPublicWithTeam": { - "title": "HeroPublicWithTeam", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - "team": IsDict( - { - "anyOf": [ - {"$ref": "#/components/schemas/TeamPublic"}, - {"type": "null"}, - ] - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"$ref": "#/components/schemas/TeamPublic"} - ), - }, - }, - "HeroUpdate": { - "title": "HeroUpdate", - "type": "object", - "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "title": "Secret Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), - }, - }, - "TeamCreate": { - "title": "TeamCreate", - "required": ["name", "headquarters"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "headquarters": {"title": "Headquarters", "type": "string"}, - }, - }, - "TeamPublic": { - "title": "TeamPublic", - "required": ["name", "headquarters", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "headquarters": {"title": "Headquarters", "type": "string"}, - "id": {"title": "Id", "type": "integer"}, - }, - }, - "TeamPublicWithHeroes": { - "title": "TeamPublicWithHeroes", - "required": ["name", "headquarters", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "headquarters": {"title": "Headquarters", "type": "string"}, - "id": {"title": "Id", "type": "integer"}, - "heroes": { - "title": "Heroes", - "type": "array", - "items": {"$ref": "#/components/schemas/HeroPublic"}, - "default": [], - }, - }, - }, - "TeamUpdate": { - "title": "TeamUpdate", - "type": "object", - "properties": { - "id": IsDict( - { - "title": "Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Id", "type": "integer"} - ), - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "headquarters": IsDict( - { - "title": "Headquarters", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Headquarters", "type": "string"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py39.py deleted file mode 100644 index 5ef7338d44..0000000000 --- a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001_py39.py +++ /dev/null @@ -1,767 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.relationships import tutorial001_py39 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - team_preventers = {"name": "Preventers", "headquarters": "Sharp Tower"} - team_z_force = {"name": "Z-Force", "headquarters": "Sister Margaret's Bar"} - response = client.post("/teams/", json=team_preventers) - assert response.status_code == 200, response.text - team_preventers_data = response.json() - team_preventers_id = team_preventers_data["id"] - response = client.post("/teams/", json=team_z_force) - assert response.status_code == 200, response.text - team_z_force_data = response.json() - team_z_force_id = team_z_force_data["id"] - response = client.get("/teams/") - data = response.json() - assert len(data) == 2 - response = client.get("/teams/9000") - assert response.status_code == 404, response.text - response = client.patch( - f"/teams/{team_preventers_id}", json={"headquarters": "Preventers Tower"} - ) - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == team_preventers["name"] - assert data["headquarters"] == "Preventers Tower" - response = client.patch("/teams/9000", json={"name": "Freedom League"}) - assert response.status_code == 404, response.text - - hero1_data = { - "name": "Deadpond", - "secret_name": "Dive Wilson", - "team_id": team_z_force_id, - } - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - hero3_data = { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - "team_id": team_preventers_id, - } - response = client.post("/heroes/", json=hero1_data) - assert response.status_code == 200, response.text - hero1 = response.json() - hero1_id = hero1["id"] - response = client.post("/heroes/", json=hero2_data) - assert response.status_code == 200, response.text - hero2 = response.json() - hero2_id = hero2["id"] - response = client.post("/heroes/", json=hero3_data) - assert response.status_code == 200, response.text - response = client.get("/heroes/9000") - assert response.status_code == 404, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 3 - response = client.get(f"/heroes/{hero1_id}") - assert response.status_code == 200, response.text - data = response.json() - assert data["name"] == hero1_data["name"] - assert data["team"]["name"] == team_z_force["name"] - response = client.patch( - f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} - ) - assert response.status_code == 200, response.text - response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) - assert response.status_code == 404, response.text - response = client.delete(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 2 - response = client.delete("/heroes/9000") - assert response.status_code == 404, response.text - - response = client.get(f"/teams/{team_preventers_id}") - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == team_preventers_data["name"] - assert data["heroes"][0]["name"] == hero3_data["name"] - - response = client.delete(f"/teams/{team_preventers_id}") - assert response.status_code == 200, response.text - response = client.delete("/teams/9000") - assert response.status_code == 404, response.text - response = client.get("/teams/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 1 - - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100.0, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublicWithTeam" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "delete": { - "summary": "Delete Hero", - "operationId": "delete_hero_heroes__hero_id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Hero", - "operationId": "update_hero_heroes__hero_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroUpdate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/teams/": { - "get": { - "summary": "Read Teams", - "operationId": "read_teams_teams__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100.0, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Teams Teams Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/TeamPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Team", - "operationId": "create_team_teams__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/teams/{team_id}": { - "get": { - "summary": "Read Team", - "operationId": "read_team_teams__team_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Team Id", "type": "integer"}, - "name": "team_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamPublicWithHeroes" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "delete": { - "summary": "Delete Team", - "operationId": "delete_team_teams__team_id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Team Id", "type": "integer"}, - "name": "team_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Team", - "operationId": "update_team_teams__team_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Team Id", "type": "integer"}, - "name": "team_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamUpdate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "HeroPublicWithTeam": { - "title": "HeroPublicWithTeam", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - "team": IsDict( - { - "anyOf": [ - {"$ref": "#/components/schemas/TeamPublic"}, - {"type": "null"}, - ] - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"$ref": "#/components/schemas/TeamPublic"} - ), - }, - }, - "HeroUpdate": { - "title": "HeroUpdate", - "type": "object", - "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "title": "Secret Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), - }, - }, - "TeamCreate": { - "title": "TeamCreate", - "required": ["name", "headquarters"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "headquarters": {"title": "Headquarters", "type": "string"}, - }, - }, - "TeamPublic": { - "title": "TeamPublic", - "required": ["name", "headquarters", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "headquarters": {"title": "Headquarters", "type": "string"}, - "id": {"title": "Id", "type": "integer"}, - }, - }, - "TeamPublicWithHeroes": { - "title": "TeamPublicWithHeroes", - "required": ["name", "headquarters", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "headquarters": {"title": "Headquarters", "type": "string"}, - "id": {"title": "Id", "type": "integer"}, - "heroes": { - "title": "Heroes", - "type": "array", - "items": {"$ref": "#/components/schemas/HeroPublic"}, - "default": [], - }, - }, - }, - "TeamUpdate": { - "title": "TeamUpdate", - "type": "object", - "properties": { - "id": IsDict( - { - "title": "Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Id", "type": "integer"} - ), - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "headquarters": IsDict( - { - "title": "Headquarters", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Headquarters", "type": "string"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py index 8f273bbd93..186a120311 100644 --- a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py @@ -1,18 +1,36 @@ +import importlib +from types import ModuleType + +import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool +from tests.conftest import needs_py39, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.response_model import tutorial001 as mod +@pytest.fixture( + name="module", + params=[ + "tutorial001", + pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.fastapi.response_model.{request.param}" + ) mod.sqlite_url = "sqlite://" mod.engine = create_engine( mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool ) + return mod + - with TestClient(mod.app) as client: +def test_tutorial(module: ModuleType): + with TestClient(module.app) as client: hero_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} response = client.post("/heroes/", json=hero_data) data = response.json() diff --git a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py310.py deleted file mode 100644 index d249cc4e90..0000000000 --- a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py310.py +++ /dev/null @@ -1,162 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.response_model import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - response = client.post("/heroes/", json=hero_data) - data = response.json() - - assert response.status_code == 200, response.text - assert data["name"] == hero_data["name"] - assert data["secret_name"] == hero_data["secret_name"] - assert data["id"] is not None - assert data["age"] is None - - response = client.get("/heroes/") - data = response.json() - - assert response.status_code == 200, response.text - assert len(data) == 1 - assert data[0]["name"] == hero_data["name"] - assert data[0]["secret_name"] == hero_data["secret_name"] - - response = client.get("/openapi.json") - - assert response.status_code == 200, response.text - - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/Hero" - }, - } - } - }, - } - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Hero"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Hero"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "Hero": { - "title": "Hero", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "id": IsDict( - { - "title": "Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Id", "type": "integer"} - ), - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py39.py deleted file mode 100644 index b9fb2be03f..0000000000 --- a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001_py39.py +++ /dev/null @@ -1,162 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.response_model import tutorial001_py39 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - response = client.post("/heroes/", json=hero_data) - data = response.json() - - assert response.status_code == 200, response.text - assert data["name"] == hero_data["name"] - assert data["secret_name"] == hero_data["secret_name"] - assert data["id"] is not None - assert data["age"] is None - - response = client.get("/heroes/") - data = response.json() - - assert response.status_code == 200, response.text - assert len(data) == 1 - assert data[0]["name"] == hero_data["name"] - assert data[0]["secret_name"] == hero_data["secret_name"] - - response = client.get("/openapi.json") - - assert response.status_code == 200, response.text - - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/Hero" - }, - } - } - }, - } - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Hero"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Hero"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "Hero": { - "title": "Hero", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "id": IsDict( - { - "title": "Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Id", "type": "integer"} - ), - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py index 388cfa9b2b..184fb219a9 100644 --- a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py @@ -1,18 +1,36 @@ +import importlib +from types import ModuleType + +import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool +from tests.conftest import needs_py39, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.session_with_dependency import tutorial001 as mod +@pytest.fixture( + name="module", + params=[ + "tutorial001", + pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.fastapi.session_with_dependency.{request.param}" + ) mod.sqlite_url = "sqlite://" mod.engine = create_engine( mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool ) + return mod + - with TestClient(mod.app) as client: +def test_tutorial(module: ModuleType): + with TestClient(module.app) as client: hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} hero2_data = { "name": "Spider-Boy", diff --git a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py310.py deleted file mode 100644 index 65bab47735..0000000000 --- a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py310.py +++ /dev/null @@ -1,379 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.session_with_dependency import ( - tutorial001_py310 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - hero3_data = { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - } - response = client.post("/heroes/", json=hero1_data) - assert response.status_code == 200, response.text - response = client.post("/heroes/", json=hero2_data) - assert response.status_code == 200, response.text - hero2 = response.json() - hero2_id = hero2["id"] - response = client.post("/heroes/", json=hero3_data) - assert response.status_code == 200, response.text - response = client.get(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/9000") - assert response.status_code == 404, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 3 - response = client.patch( - f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} - ) - assert response.status_code == 200, response.text - response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) - assert response.status_code == 404, response.text - - response = client.delete(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 2 - - response = client.delete("/heroes/9000") - assert response.status_code == 404, response.text - - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100.0, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "delete": { - "summary": "Delete Hero", - "operationId": "delete_hero_heroes__hero_id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Hero", - "operationId": "update_hero_heroes__hero_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroUpdate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "HeroUpdate": { - "title": "HeroUpdate", - "type": "object", - "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "title": "Secret Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py39.py deleted file mode 100644 index cdab85df17..0000000000 --- a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001_py39.py +++ /dev/null @@ -1,379 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.session_with_dependency import ( - tutorial001_py39 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - hero3_data = { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - } - response = client.post("/heroes/", json=hero1_data) - assert response.status_code == 200, response.text - response = client.post("/heroes/", json=hero2_data) - assert response.status_code == 200, response.text - hero2 = response.json() - hero2_id = hero2["id"] - response = client.post("/heroes/", json=hero3_data) - assert response.status_code == 200, response.text - response = client.get(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/9000") - assert response.status_code == 404, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 3 - response = client.patch( - f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} - ) - assert response.status_code == 200, response.text - response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) - assert response.status_code == 404, response.text - - response = client.delete(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 2 - - response = client.delete("/heroes/9000") - assert response.status_code == 404, response.text - - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100.0, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "delete": { - "summary": "Delete Hero", - "operationId": "delete_hero_heroes__hero_id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Hero", - "operationId": "update_hero_heroes__hero_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroUpdate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "HeroUpdate": { - "title": "HeroUpdate", - "type": "object", - "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "title": "Secret Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py index 9df7e50b81..dba7bc3879 100644 --- a/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py @@ -1,18 +1,35 @@ +import importlib +from types import ModuleType + +import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool +from tests.conftest import needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.simple_hero_api import tutorial001 as mod +@pytest.fixture( + name="module", + params=[ + "tutorial001", + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.fastapi.simple_hero_api.{request.param}" + ) mod.sqlite_url = "sqlite://" mod.engine = create_engine( mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool ) + return mod + - with TestClient(mod.app) as client: +def test_tutorial(module: ModuleType): + with TestClient(module.app) as client: hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} hero2_data = { "name": "Spider-Boy", diff --git a/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001_py310.py deleted file mode 100644 index a47513dde2..0000000000 --- a/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001_py310.py +++ /dev/null @@ -1,168 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.simple_hero_api import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - response = client.post("/heroes/", json=hero1_data) - data = response.json() - - assert response.status_code == 200, response.text - assert data["name"] == hero1_data["name"] - assert data["secret_name"] == hero1_data["secret_name"] - assert data["id"] is not None - assert data["age"] is None - - response = client.post("/heroes/", json=hero2_data) - data = response.json() - - assert response.status_code == 200, response.text - assert data["name"] == hero2_data["name"] - assert data["secret_name"] == hero2_data["secret_name"] - assert data["id"] == hero2_data["id"], ( - "Up to this point it's still possible to " - "set the ID of the hero in the request" - ) - assert data["age"] is None - - response = client.get("/heroes/") - data = response.json() - - assert response.status_code == 200, response.text - assert len(data) == 2 - assert data[0]["name"] == hero1_data["name"] - assert data[0]["secret_name"] == hero1_data["secret_name"] - assert data[1]["name"] == hero2_data["name"] - assert data[1]["secret_name"] == hero2_data["secret_name"] - assert data[1]["id"] == hero2_data["id"] - - response = client.get("/openapi.json") - - assert response.status_code == 200, response.text - - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Hero"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "Hero": { - "title": "Hero", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "id": IsDict( - { - "title": "Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Id", "type": "integer"} - ), - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py index 25daadf74b..2e7d66a036 100644 --- a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py @@ -1,18 +1,34 @@ +import importlib +from types import ModuleType + +import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool +from tests.conftest import needs_py39, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.teams import tutorial001 as mod +@pytest.fixture( + name="module", + params=[ + "tutorial001", + pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.fastapi.teams.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine( mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool ) + return mod + - with TestClient(mod.app) as client: +def test_tutorial(module: ModuleType): + with TestClient(module.app) as client: hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} hero2_data = { "name": "Spider-Boy", diff --git a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py310.py deleted file mode 100644 index 63f8a1d70b..0000000000 --- a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py310.py +++ /dev/null @@ -1,686 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.teams import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - hero3_data = { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - } - response = client.post("/heroes/", json=hero1_data) - assert response.status_code == 200, response.text - response = client.post("/heroes/", json=hero2_data) - assert response.status_code == 200, response.text - hero2 = response.json() - hero2_id = hero2["id"] - response = client.post("/heroes/", json=hero3_data) - assert response.status_code == 200, response.text - response = client.get(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/9000") - assert response.status_code == 404, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 3 - response = client.patch( - f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} - ) - assert response.status_code == 200, response.text - response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) - assert response.status_code == 404, response.text - response = client.delete(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 2 - response = client.delete("/heroes/9000") - assert response.status_code == 404, response.text - - team_preventers = {"name": "Preventers", "headquarters": "Sharp Tower"} - team_z_force = {"name": "Z-Force", "headquarters": "Sister Margaret's Bar"} - response = client.post("/teams/", json=team_preventers) - assert response.status_code == 200, response.text - team_preventers_data = response.json() - team_preventers_id = team_preventers_data["id"] - response = client.post("/teams/", json=team_z_force) - assert response.status_code == 200, response.text - team_z_force_data = response.json() - team_z_force_data["id"] - response = client.get("/teams/") - data = response.json() - assert len(data) == 2 - response = client.get(f"/teams/{team_preventers_id}") - data = response.json() - assert response.status_code == 200, response.text - assert data == team_preventers_data - response = client.get("/teams/9000") - assert response.status_code == 404, response.text - response = client.patch( - f"/teams/{team_preventers_id}", json={"headquarters": "Preventers Tower"} - ) - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == team_preventers["name"] - assert data["headquarters"] == "Preventers Tower" - response = client.patch("/teams/9000", json={"name": "Freedom League"}) - assert response.status_code == 404, response.text - response = client.delete(f"/teams/{team_preventers_id}") - assert response.status_code == 200, response.text - response = client.delete("/teams/9000") - assert response.status_code == 404, response.text - response = client.get("/teams/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 1 - - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100.0, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "delete": { - "summary": "Delete Hero", - "operationId": "delete_hero_heroes__hero_id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Hero", - "operationId": "update_hero_heroes__hero_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroUpdate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/teams/": { - "get": { - "summary": "Read Teams", - "operationId": "read_teams_teams__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100.0, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Teams Teams Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/TeamPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Team", - "operationId": "create_team_teams__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/teams/{team_id}": { - "get": { - "summary": "Read Team", - "operationId": "read_team_teams__team_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Team Id", "type": "integer"}, - "name": "team_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "delete": { - "summary": "Delete Team", - "operationId": "delete_team_teams__team_id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Team Id", "type": "integer"}, - "name": "team_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Team", - "operationId": "update_team_teams__team_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Team Id", "type": "integer"}, - "name": "team_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamUpdate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "HeroUpdate": { - "title": "HeroUpdate", - "type": "object", - "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "title": "Secret Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), - }, - }, - "TeamCreate": { - "title": "TeamCreate", - "required": ["name", "headquarters"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "headquarters": {"title": "Headquarters", "type": "string"}, - }, - }, - "TeamPublic": { - "title": "TeamPublic", - "required": ["name", "headquarters", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "headquarters": {"title": "Headquarters", "type": "string"}, - "id": {"title": "Id", "type": "integer"}, - }, - }, - "TeamUpdate": { - "title": "TeamUpdate", - "type": "object", - "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "headquarters": IsDict( - { - "title": "Headquarters", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Headquarters", "type": "string"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py39.py deleted file mode 100644 index 30b68e0ed9..0000000000 --- a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001_py39.py +++ /dev/null @@ -1,686 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.teams import tutorial001_py39 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - hero3_data = { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - } - response = client.post("/heroes/", json=hero1_data) - assert response.status_code == 200, response.text - response = client.post("/heroes/", json=hero2_data) - assert response.status_code == 200, response.text - hero2 = response.json() - hero2_id = hero2["id"] - response = client.post("/heroes/", json=hero3_data) - assert response.status_code == 200, response.text - response = client.get(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/9000") - assert response.status_code == 404, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 3 - response = client.patch( - f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} - ) - assert response.status_code == 200, response.text - response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) - assert response.status_code == 404, response.text - response = client.delete(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 2 - response = client.delete("/heroes/9000") - assert response.status_code == 404, response.text - - team_preventers = {"name": "Preventers", "headquarters": "Sharp Tower"} - team_z_force = {"name": "Z-Force", "headquarters": "Sister Margaret's Bar"} - response = client.post("/teams/", json=team_preventers) - assert response.status_code == 200, response.text - team_preventers_data = response.json() - team_preventers_id = team_preventers_data["id"] - response = client.post("/teams/", json=team_z_force) - assert response.status_code == 200, response.text - team_z_force_data = response.json() - team_z_force_data["id"] - response = client.get("/teams/") - data = response.json() - assert len(data) == 2 - response = client.get(f"/teams/{team_preventers_id}") - data = response.json() - assert response.status_code == 200, response.text - assert data == team_preventers_data - response = client.get("/teams/9000") - assert response.status_code == 404, response.text - response = client.patch( - f"/teams/{team_preventers_id}", json={"headquarters": "Preventers Tower"} - ) - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == team_preventers["name"] - assert data["headquarters"] == "Preventers Tower" - response = client.patch("/teams/9000", json={"name": "Freedom League"}) - assert response.status_code == 404, response.text - response = client.delete(f"/teams/{team_preventers_id}") - assert response.status_code == 200, response.text - response = client.delete("/teams/9000") - assert response.status_code == 404, response.text - response = client.get("/teams/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 1 - - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100.0, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "delete": { - "summary": "Delete Hero", - "operationId": "delete_hero_heroes__hero_id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Hero", - "operationId": "update_hero_heroes__hero_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroUpdate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/teams/": { - "get": { - "summary": "Read Teams", - "operationId": "read_teams_teams__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100.0, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Teams Teams Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/TeamPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Team", - "operationId": "create_team_teams__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/teams/{team_id}": { - "get": { - "summary": "Read Team", - "operationId": "read_team_teams__team_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Team Id", "type": "integer"}, - "name": "team_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "delete": { - "summary": "Delete Team", - "operationId": "delete_team_teams__team_id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Team Id", "type": "integer"}, - "name": "team_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Team", - "operationId": "update_team_teams__team_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Team Id", "type": "integer"}, - "name": "team_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamUpdate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TeamPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "HeroUpdate": { - "title": "HeroUpdate", - "type": "object", - "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "title": "Secret Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), - }, - }, - "TeamCreate": { - "title": "TeamCreate", - "required": ["name", "headquarters"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "headquarters": {"title": "Headquarters", "type": "string"}, - }, - }, - "TeamPublic": { - "title": "TeamPublic", - "required": ["name", "headquarters", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "headquarters": {"title": "Headquarters", "type": "string"}, - "id": {"title": "Id", "type": "integer"}, - }, - }, - "TeamUpdate": { - "title": "TeamUpdate", - "type": "object", - "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "headquarters": IsDict( - { - "title": "Headquarters", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Headquarters", "type": "string"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py index 6f3856912e..b6ee27611f 100644 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py @@ -1,18 +1,34 @@ +import importlib +from types import ModuleType + +import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool +from tests.conftest import needs_py39, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.update import tutorial001 as mod +@pytest.fixture( + name="module", + params=[ + "tutorial001", + pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.fastapi.update.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine( mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool ) + return mod + - with TestClient(mod.app) as client: +def test_tutorial(module: ModuleType): + with TestClient(module.app) as client: hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} hero2_data = { "name": "Spider-Boy", diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py310.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py310.py deleted file mode 100644 index 119634dc1e..0000000000 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py310.py +++ /dev/null @@ -1,356 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.update import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - hero3_data = { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - } - response = client.post("/heroes/", json=hero1_data) - assert response.status_code == 200, response.text - response = client.post("/heroes/", json=hero2_data) - assert response.status_code == 200, response.text - hero2 = response.json() - hero2_id = hero2["id"] - response = client.post("/heroes/", json=hero3_data) - assert response.status_code == 200, response.text - hero3 = response.json() - hero3_id = hero3["id"] - response = client.get(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/9000") - assert response.status_code == 404, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 3 - - response = client.patch( - f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} - ) - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == hero2_data["name"], "The name should not be set to none" - assert data["secret_name"] == "Spider-Youngster", ( - "The secret name should be updated" - ) - - response = client.patch(f"/heroes/{hero3_id}", json={"age": None}) - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == hero3_data["name"] - assert data["age"] is None, ( - "A field should be updatable to None, even if that's the default" - ) - - response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) - assert response.status_code == 404, response.text - - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100.0, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Hero", - "operationId": "update_hero_heroes__hero_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroUpdate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "HeroUpdate": { - "title": "HeroUpdate", - "type": "object", - "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "title": "Secret Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py39.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py39.py deleted file mode 100644 index 455480f735..0000000000 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001_py39.py +++ /dev/null @@ -1,356 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.update import tutorial001_py39 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = {"name": "Deadpond", "secret_name": "Dive Wilson"} - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - } - hero3_data = { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - } - response = client.post("/heroes/", json=hero1_data) - assert response.status_code == 200, response.text - response = client.post("/heroes/", json=hero2_data) - assert response.status_code == 200, response.text - hero2 = response.json() - hero2_id = hero2["id"] - response = client.post("/heroes/", json=hero3_data) - assert response.status_code == 200, response.text - hero3 = response.json() - hero3_id = hero3["id"] - response = client.get(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - response = client.get("/heroes/9000") - assert response.status_code == 404, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 3 - - response = client.patch( - f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} - ) - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == hero2_data["name"], "The name should not be set to none" - assert data["secret_name"] == "Spider-Youngster", ( - "The secret name should be updated" - ) - - response = client.patch(f"/heroes/{hero3_id}", json={"age": None}) - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == hero3_data["name"] - assert data["age"] is None, ( - "A field should be updatable to None, even if that's the default" - ) - - response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) - assert response.status_code == 404, response.text - - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100.0, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Hero", - "operationId": "update_hero_heroes__hero_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroUpdate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "HeroUpdate": { - "title": "HeroUpdate", - "type": "object", - "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "title": "Secret Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial002.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial002.py index 2a929f6dae..a9312651b7 100644 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial002.py +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial002.py @@ -1,18 +1,34 @@ +import importlib +from types import ModuleType + +import pytest from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import Session, create_engine from sqlmodel.pool import StaticPool +from tests.conftest import needs_py39, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.update import tutorial002 as mod +@pytest.fixture( + name="module", + params=[ + "tutorial002", + pytest.param("tutorial002_py39", marks=needs_py39), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.fastapi.update.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine( mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool ) + return mod + - with TestClient(mod.app) as client: +def test_tutorial(module: ModuleType): + with TestClient(module.app) as client: hero1_data = { "name": "Deadpond", "secret_name": "Dive Wilson", @@ -60,16 +76,16 @@ def test_tutorial(clear_sqlmodel): assert "hashed_password" not in response_hero # Test hashed passwords - with Session(mod.engine) as session: - hero1_db = session.get(mod.Hero, hero1_id) + with Session(module.engine) as session: + hero1_db = session.get(module.Hero, hero1_id) assert hero1_db assert not hasattr(hero1_db, "password") assert hero1_db.hashed_password == "not really hashed chimichanga hehehe" - hero2_db = session.get(mod.Hero, hero2_id) + hero2_db = session.get(module.Hero, hero2_id) assert hero2_db assert not hasattr(hero2_db, "password") assert hero2_db.hashed_password == "not really hashed auntmay hehehe" - hero3_db = session.get(mod.Hero, hero3_id) + hero3_db = session.get(module.Hero, hero3_id) assert hero3_db assert not hasattr(hero3_db, "password") assert hero3_db.hashed_password == "not really hashed bestpreventer hehehe" @@ -85,8 +101,8 @@ def test_tutorial(clear_sqlmodel): ) assert "password" not in data assert "hashed_password" not in data - with Session(mod.engine) as session: - hero2b_db = session.get(mod.Hero, hero2_id) + with Session(module.engine) as session: + hero2b_db = session.get(module.Hero, hero2_id) assert hero2b_db assert not hasattr(hero2b_db, "password") assert hero2b_db.hashed_password == "not really hashed auntmay hehehe" @@ -100,8 +116,8 @@ def test_tutorial(clear_sqlmodel): ) assert "password" not in data assert "hashed_password" not in data - with Session(mod.engine) as session: - hero3b_db = session.get(mod.Hero, hero3_id) + with Session(module.engine) as session: + hero3b_db = session.get(module.Hero, hero3_id) assert hero3b_db assert not hasattr(hero3b_db, "password") assert hero3b_db.hashed_password == "not really hashed bestpreventer hehehe" @@ -116,8 +132,8 @@ def test_tutorial(clear_sqlmodel): assert data["age"] is None assert "password" not in data assert "hashed_password" not in data - with Session(mod.engine) as session: - hero3b_db = session.get(mod.Hero, hero3_id) + with Session(module.engine) as session: + hero3b_db = session.get(module.Hero, hero3_id) assert hero3b_db assert not hasattr(hero3b_db, "password") assert ( diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py310.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py310.py deleted file mode 100644 index 7617f14996..0000000000 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py310.py +++ /dev/null @@ -1,430 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import Session, create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.update import tutorial002_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = { - "name": "Deadpond", - "secret_name": "Dive Wilson", - "password": "chimichanga", - } - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - "password": "auntmay", - } - hero3_data = { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - "password": "bestpreventer", - } - response = client.post("/heroes/", json=hero1_data) - assert response.status_code == 200, response.text - hero1 = response.json() - assert "password" not in hero1 - assert "hashed_password" not in hero1 - hero1_id = hero1["id"] - response = client.post("/heroes/", json=hero2_data) - assert response.status_code == 200, response.text - hero2 = response.json() - hero2_id = hero2["id"] - response = client.post("/heroes/", json=hero3_data) - assert response.status_code == 200, response.text - hero3 = response.json() - hero3_id = hero3["id"] - response = client.get(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - fetched_hero2 = response.json() - assert "password" not in fetched_hero2 - assert "hashed_password" not in fetched_hero2 - response = client.get("/heroes/9000") - assert response.status_code == 404, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 3 - for response_hero in data: - assert "password" not in response_hero - assert "hashed_password" not in response_hero - - # Test hashed passwords - with Session(mod.engine) as session: - hero1_db = session.get(mod.Hero, hero1_id) - assert hero1_db - assert not hasattr(hero1_db, "password") - assert hero1_db.hashed_password == "not really hashed chimichanga hehehe" - hero2_db = session.get(mod.Hero, hero2_id) - assert hero2_db - assert not hasattr(hero2_db, "password") - assert hero2_db.hashed_password == "not really hashed auntmay hehehe" - hero3_db = session.get(mod.Hero, hero3_id) - assert hero3_db - assert not hasattr(hero3_db, "password") - assert hero3_db.hashed_password == "not really hashed bestpreventer hehehe" - - response = client.patch( - f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} - ) - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == hero2_data["name"], "The name should not be set to none" - assert data["secret_name"] == "Spider-Youngster", ( - "The secret name should be updated" - ) - assert "password" not in data - assert "hashed_password" not in data - with Session(mod.engine) as session: - hero2b_db = session.get(mod.Hero, hero2_id) - assert hero2b_db - assert not hasattr(hero2b_db, "password") - assert hero2b_db.hashed_password == "not really hashed auntmay hehehe" - - response = client.patch(f"/heroes/{hero3_id}", json={"age": None}) - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == hero3_data["name"] - assert data["age"] is None, ( - "A field should be updatable to None, even if that's the default" - ) - assert "password" not in data - assert "hashed_password" not in data - with Session(mod.engine) as session: - hero3b_db = session.get(mod.Hero, hero3_id) - assert hero3b_db - assert not hasattr(hero3b_db, "password") - assert hero3b_db.hashed_password == "not really hashed bestpreventer hehehe" - - # Test update dict, hashed_password - response = client.patch( - f"/heroes/{hero3_id}", json={"password": "philantroplayboy"} - ) - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == hero3_data["name"] - assert data["age"] is None - assert "password" not in data - assert "hashed_password" not in data - with Session(mod.engine) as session: - hero3b_db = session.get(mod.Hero, hero3_id) - assert hero3b_db - assert not hasattr(hero3b_db, "password") - assert ( - hero3b_db.hashed_password == "not really hashed philantroplayboy hehehe" - ) - - response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) - assert response.status_code == 404, response.text - - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Hero", - "operationId": "update_hero_heroes__hero_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroUpdate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name", "password"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Age", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "password": {"type": "string", "title": "Password"}, - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Age", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "HeroUpdate": { - "title": "HeroUpdate", - "type": "object", - "properties": { - "name": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Name", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Secret Name", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Age", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "password": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Password", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Password", "type": "string"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py39.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py39.py deleted file mode 100644 index dc788a29f7..0000000000 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial002_py39.py +++ /dev/null @@ -1,430 +0,0 @@ -from dirty_equals import IsDict -from fastapi.testclient import TestClient -from sqlmodel import Session, create_engine -from sqlmodel.pool import StaticPool - -from ....conftest import needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.fastapi.update import tutorial002_py39 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine( - mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool - ) - - with TestClient(mod.app) as client: - hero1_data = { - "name": "Deadpond", - "secret_name": "Dive Wilson", - "password": "chimichanga", - } - hero2_data = { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "id": 9000, - "password": "auntmay", - } - hero3_data = { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "age": 48, - "password": "bestpreventer", - } - response = client.post("/heroes/", json=hero1_data) - assert response.status_code == 200, response.text - hero1 = response.json() - assert "password" not in hero1 - assert "hashed_password" not in hero1 - hero1_id = hero1["id"] - response = client.post("/heroes/", json=hero2_data) - assert response.status_code == 200, response.text - hero2 = response.json() - hero2_id = hero2["id"] - response = client.post("/heroes/", json=hero3_data) - assert response.status_code == 200, response.text - hero3 = response.json() - hero3_id = hero3["id"] - response = client.get(f"/heroes/{hero2_id}") - assert response.status_code == 200, response.text - fetched_hero2 = response.json() - assert "password" not in fetched_hero2 - assert "hashed_password" not in fetched_hero2 - response = client.get("/heroes/9000") - assert response.status_code == 404, response.text - response = client.get("/heroes/") - assert response.status_code == 200, response.text - data = response.json() - assert len(data) == 3 - for response_hero in data: - assert "password" not in response_hero - assert "hashed_password" not in response_hero - - # Test hashed passwords - with Session(mod.engine) as session: - hero1_db = session.get(mod.Hero, hero1_id) - assert hero1_db - assert not hasattr(hero1_db, "password") - assert hero1_db.hashed_password == "not really hashed chimichanga hehehe" - hero2_db = session.get(mod.Hero, hero2_id) - assert hero2_db - assert not hasattr(hero2_db, "password") - assert hero2_db.hashed_password == "not really hashed auntmay hehehe" - hero3_db = session.get(mod.Hero, hero3_id) - assert hero3_db - assert not hasattr(hero3_db, "password") - assert hero3_db.hashed_password == "not really hashed bestpreventer hehehe" - - response = client.patch( - f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"} - ) - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == hero2_data["name"], "The name should not be set to none" - assert data["secret_name"] == "Spider-Youngster", ( - "The secret name should be updated" - ) - assert "password" not in data - assert "hashed_password" not in data - with Session(mod.engine) as session: - hero2b_db = session.get(mod.Hero, hero2_id) - assert hero2b_db - assert not hasattr(hero2b_db, "password") - assert hero2b_db.hashed_password == "not really hashed auntmay hehehe" - - response = client.patch(f"/heroes/{hero3_id}", json={"age": None}) - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == hero3_data["name"] - assert data["age"] is None, ( - "A field should be updatable to None, even if that's the default" - ) - assert "password" not in data - assert "hashed_password" not in data - with Session(mod.engine) as session: - hero3b_db = session.get(mod.Hero, hero3_id) - assert hero3b_db - assert not hasattr(hero3b_db, "password") - assert hero3b_db.hashed_password == "not really hashed bestpreventer hehehe" - - # Test update dict, hashed_password - response = client.patch( - f"/heroes/{hero3_id}", json={"password": "philantroplayboy"} - ) - data = response.json() - assert response.status_code == 200, response.text - assert data["name"] == hero3_data["name"] - assert data["age"] is None - assert "password" not in data - assert "hashed_password" not in data - with Session(mod.engine) as session: - hero3b_db = session.get(mod.Hero, hero3_id) - assert hero3b_db - assert not hasattr(hero3b_db, "password") - assert ( - hero3b_db.hashed_password == "not really hashed philantroplayboy hehehe" - ) - - response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"}) - assert response.status_code == 404, response.text - - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/heroes/": { - "get": { - "summary": "Read Heroes", - "operationId": "read_heroes_heroes__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Offset", - "type": "integer", - "default": 0, - }, - "name": "offset", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "maximum": 100, - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Heroes Heroes Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/HeroPublic" - }, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create Hero", - "operationId": "create_hero_heroes__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroCreate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/heroes/{hero_id}": { - "get": { - "summary": "Read Hero", - "operationId": "read_hero_heroes__hero_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "patch": { - "summary": "Update Hero", - "operationId": "update_hero_heroes__hero_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Hero Id", "type": "integer"}, - "name": "hero_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroUpdate" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HeroPublic" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - } - }, - }, - "HeroCreate": { - "title": "HeroCreate", - "required": ["name", "secret_name", "password"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Age", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "password": {"type": "string", "title": "Password"}, - }, - }, - "HeroPublic": { - "title": "HeroPublic", - "required": ["name", "secret_name", "id"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Age", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "id": {"title": "Id", "type": "integer"}, - }, - }, - "HeroUpdate": { - "title": "HeroUpdate", - "type": "object", - "properties": { - "name": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Name", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Secret Name", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Age", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "password": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Password", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Password", "type": "string"} - ), - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } diff --git a/tests/test_tutorial/test_indexes/test_tutorial001.py b/tests/test_tutorial/test_indexes/test_tutorial001.py index f33db5bcc7..b71f7ebd85 100644 --- a/tests/test_tutorial/test_indexes/test_tutorial001.py +++ b/tests/test_tutorial/test_indexes/test_tutorial001.py @@ -1,24 +1,31 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlalchemy import inspect from sqlalchemy.engine.reflection import Inspector from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.indexes import tutorial001 as mod - +@pytest.fixture( + name="mod", + params=[ + "tutorial001", + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.indexes.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [{"secret_name": "Dive Wilson", "age": None, "id": 1, "name": "Deadpond"}] ] diff --git a/tests/test_tutorial/test_indexes/test_tutorial001_py310.py b/tests/test_tutorial/test_indexes/test_tutorial001_py310.py deleted file mode 100644 index cfee262b2b..0000000000 --- a/tests/test_tutorial/test_indexes/test_tutorial001_py310.py +++ /dev/null @@ -1,46 +0,0 @@ -from unittest.mock import patch - -from sqlalchemy import inspect -from sqlalchemy.engine.reflection import Inspector -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.indexes import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [{"secret_name": "Dive Wilson", "age": None, "id": 1, "name": "Deadpond"}] - ] - - insp: Inspector = inspect(mod.engine) - indexes = insp.get_indexes(str(mod.Hero.__tablename__)) - expected_indexes = [ - { - "name": "ix_hero_name", - "dialect_options": {}, - "column_names": ["name"], - "unique": 0, - }, - { - "name": "ix_hero_age", - "dialect_options": {}, - "column_names": ["age"], - "unique": 0, - }, - ] - for index in expected_indexes: - assert index in indexes, "This expected index should be in the indexes in DB" - # Now that this index was checked, remove it from the list of indexes - indexes.pop(indexes.index(index)) - assert len(indexes) == 0, "The database should only have the expected indexes" diff --git a/tests/test_tutorial/test_indexes/test_tutorial002.py b/tests/test_tutorial/test_indexes/test_tutorial002.py index 893043dad1..0aa88ea5dd 100644 --- a/tests/test_tutorial/test_indexes/test_tutorial002.py +++ b/tests/test_tutorial/test_indexes/test_tutorial002.py @@ -1,24 +1,31 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlalchemy import inspect from sqlalchemy.engine.reflection import Inspector from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.indexes import tutorial002 as mod - +@pytest.fixture( + name="mod", + params=[ + "tutorial002", + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.indexes.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [{"name": "Tarantula", "secret_name": "Natalia Roman-on", "age": 32, "id": 4}], [{"name": "Black Lion", "secret_name": "Trevor Challa", "age": 35, "id": 5}], ] diff --git a/tests/test_tutorial/test_indexes/test_tutorial002_py310.py b/tests/test_tutorial/test_indexes/test_tutorial002_py310.py deleted file mode 100644 index 089b6828e9..0000000000 --- a/tests/test_tutorial/test_indexes/test_tutorial002_py310.py +++ /dev/null @@ -1,47 +0,0 @@ -from unittest.mock import patch - -from sqlalchemy import inspect -from sqlalchemy.engine.reflection import Inspector -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.indexes import tutorial002_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [{"name": "Tarantula", "secret_name": "Natalia Roman-on", "age": 32, "id": 4}], - [{"name": "Black Lion", "secret_name": "Trevor Challa", "age": 35, "id": 5}], - ] - - insp: Inspector = inspect(mod.engine) - indexes = insp.get_indexes(str(mod.Hero.__tablename__)) - expected_indexes = [ - { - "name": "ix_hero_name", - "dialect_options": {}, - "column_names": ["name"], - "unique": 0, - }, - { - "name": "ix_hero_age", - "dialect_options": {}, - "column_names": ["age"], - "unique": 0, - }, - ] - for index in expected_indexes: - assert index in indexes, "This expected index should be in the indexes in DB" - # Now that this index was checked, remove it from the list of indexes - indexes.pop(indexes.index(index)) - assert len(indexes) == 0, "The database should only have the expected indexes" diff --git a/tests/test_tutorial/test_insert/test_tutorial001.py b/tests/test_tutorial/test_insert/test_tutorial001.py index 3a5162c08a..1673a70753 100644 --- a/tests/test_tutorial/test_insert/test_tutorial001.py +++ b/tests/test_tutorial/test_insert/test_tutorial001.py @@ -1,11 +1,27 @@ +import importlib +from types import ModuleType + +import pytest from sqlmodel import Session, create_engine, select +from ...conftest import needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.insert import tutorial001 as mod +@pytest.fixture( + name="mod", + params=[ + "tutorial001", + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.insert.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) + return mod + + +def test_tutorial(mod: ModuleType): mod.main() with Session(mod.engine) as session: heroes = session.exec(select(mod.Hero)).all() diff --git a/tests/test_tutorial/test_insert/test_tutorial001_py310.py b/tests/test_tutorial/test_insert/test_tutorial001_py310.py deleted file mode 100644 index 47cbc4cde6..0000000000 --- a/tests/test_tutorial/test_insert/test_tutorial001_py310.py +++ /dev/null @@ -1,30 +0,0 @@ -from sqlmodel import Session, create_engine, select - -from ...conftest import needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.insert import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - mod.main() - with Session(mod.engine) as session: - heroes = session.exec(select(mod.Hero)).all() - heroes_by_name = {hero.name: hero for hero in heroes} - deadpond = heroes_by_name["Deadpond"] - spider_boy = heroes_by_name["Spider-Boy"] - rusty_man = heroes_by_name["Rusty-Man"] - assert deadpond.name == "Deadpond" - assert deadpond.age is None - assert deadpond.id is not None - assert deadpond.secret_name == "Dive Wilson" - assert spider_boy.name == "Spider-Boy" - assert spider_boy.age is None - assert spider_boy.id is not None - assert spider_boy.secret_name == "Pedro Parqueador" - assert rusty_man.name == "Rusty-Man" - assert rusty_man.age == 48 - assert rusty_man.id is not None - assert rusty_man.secret_name == "Tommy Sharp" diff --git a/tests/test_tutorial/test_insert/test_tutorial002.py b/tests/test_tutorial/test_insert/test_tutorial002.py index c450ec044d..0cee7ef3b1 100644 --- a/tests/test_tutorial/test_insert/test_tutorial002.py +++ b/tests/test_tutorial/test_insert/test_tutorial002.py @@ -1,11 +1,27 @@ +import importlib +from types import ModuleType + +import pytest from sqlmodel import Session, create_engine, select +from ...conftest import needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.insert import tutorial002 as mod +@pytest.fixture( + name="mod", + params=[ + "tutorial002", + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.insert.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) + return mod + + +def test_tutorial(mod: ModuleType): mod.main() with Session(mod.engine) as session: heroes = session.exec(select(mod.Hero)).all() diff --git a/tests/test_tutorial/test_insert/test_tutorial002_py310.py b/tests/test_tutorial/test_insert/test_tutorial002_py310.py deleted file mode 100644 index fb62810baf..0000000000 --- a/tests/test_tutorial/test_insert/test_tutorial002_py310.py +++ /dev/null @@ -1,30 +0,0 @@ -from sqlmodel import Session, create_engine, select - -from ...conftest import needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.insert import tutorial002_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - mod.main() - with Session(mod.engine) as session: - heroes = session.exec(select(mod.Hero)).all() - heroes_by_name = {hero.name: hero for hero in heroes} - deadpond = heroes_by_name["Deadpond"] - spider_boy = heroes_by_name["Spider-Boy"] - rusty_man = heroes_by_name["Rusty-Man"] - assert deadpond.name == "Deadpond" - assert deadpond.age is None - assert deadpond.id is not None - assert deadpond.secret_name == "Dive Wilson" - assert spider_boy.name == "Spider-Boy" - assert spider_boy.age is None - assert spider_boy.id is not None - assert spider_boy.secret_name == "Pedro Parqueador" - assert rusty_man.name == "Rusty-Man" - assert rusty_man.age == 48 - assert rusty_man.id is not None - assert rusty_man.secret_name == "Tommy Sharp" diff --git a/tests/test_tutorial/test_insert/test_tutorial003.py b/tests/test_tutorial/test_insert/test_tutorial003.py index df2112b25a..7f80585875 100644 --- a/tests/test_tutorial/test_insert/test_tutorial003.py +++ b/tests/test_tutorial/test_insert/test_tutorial003.py @@ -1,11 +1,27 @@ +import importlib +from types import ModuleType + +import pytest from sqlmodel import Session, create_engine, select +from ...conftest import needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.insert import tutorial003 as mod +@pytest.fixture( + name="mod", + params=[ + "tutorial003", + pytest.param("tutorial003_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.insert.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) + return mod + + +def test_tutorial(mod: ModuleType): mod.main() with Session(mod.engine) as session: heroes = session.exec(select(mod.Hero)).all() diff --git a/tests/test_tutorial/test_insert/test_tutorial003_py310.py b/tests/test_tutorial/test_insert/test_tutorial003_py310.py deleted file mode 100644 index 5bca713e60..0000000000 --- a/tests/test_tutorial/test_insert/test_tutorial003_py310.py +++ /dev/null @@ -1,30 +0,0 @@ -from sqlmodel import Session, create_engine, select - -from ...conftest import needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.insert import tutorial003_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - mod.main() - with Session(mod.engine) as session: - heroes = session.exec(select(mod.Hero)).all() - heroes_by_name = {hero.name: hero for hero in heroes} - deadpond = heroes_by_name["Deadpond"] - spider_boy = heroes_by_name["Spider-Boy"] - rusty_man = heroes_by_name["Rusty-Man"] - assert deadpond.name == "Deadpond" - assert deadpond.age is None - assert deadpond.id is not None - assert deadpond.secret_name == "Dive Wilson" - assert spider_boy.name == "Spider-Boy" - assert spider_boy.age is None - assert spider_boy.id is not None - assert spider_boy.secret_name == "Pedro Parqueador" - assert rusty_man.name == "Rusty-Man" - assert rusty_man.age == 48 - assert rusty_man.id is not None - assert rusty_man.secret_name == "Tommy Sharp" diff --git a/tests/test_tutorial/test_limit_and_offset/test_tutorial001.py b/tests/test_tutorial/test_limit_and_offset/test_tutorial001.py index 244f91083f..d33713cde3 100644 --- a/tests/test_tutorial/test_limit_and_offset/test_tutorial001.py +++ b/tests/test_tutorial/test_limit_and_offset/test_tutorial001.py @@ -1,8 +1,25 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 + + +@pytest.fixture( + name="mod", + params=[ + "tutorial001", + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.offset_and_limit.{request.param}") + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + return mod + expected_calls = [ [ @@ -20,15 +37,6 @@ ] -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.offset_and_limit import tutorial001 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_limit_and_offset/test_tutorial001_py310.py b/tests/test_tutorial/test_limit_and_offset/test_tutorial001_py310.py deleted file mode 100644 index 4f4974c853..0000000000 --- a/tests/test_tutorial/test_limit_and_offset/test_tutorial001_py310.py +++ /dev/null @@ -1,35 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - [ - {"id": 1, "name": "Deadpond", "secret_name": "Dive Wilson", "age": None}, - { - "id": 2, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "age": None, - }, - {"id": 3, "name": "Rusty-Man", "secret_name": "Tommy Sharp", "age": 48}, - ] - ] -] - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.offset_and_limit import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_limit_and_offset/test_tutorial002.py b/tests/test_tutorial/test_limit_and_offset/test_tutorial002.py index e9dee0cb35..c73f701cd1 100644 --- a/tests/test_tutorial/test_limit_and_offset/test_tutorial002.py +++ b/tests/test_tutorial/test_limit_and_offset/test_tutorial002.py @@ -1,8 +1,25 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 + + +@pytest.fixture( + name="mod", + params=[ + "tutorial002", + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.offset_and_limit.{request.param}") + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + return mod + expected_calls = [ [ @@ -20,15 +37,6 @@ ] -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.offset_and_limit import tutorial002 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_limit_and_offset/test_tutorial002_py310.py b/tests/test_tutorial/test_limit_and_offset/test_tutorial002_py310.py deleted file mode 100644 index 1f86d1960e..0000000000 --- a/tests/test_tutorial/test_limit_and_offset/test_tutorial002_py310.py +++ /dev/null @@ -1,35 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - [ - { - "id": 4, - "name": "Tarantula", - "secret_name": "Natalia Roman-on", - "age": 32, - }, - {"id": 5, "name": "Black Lion", "secret_name": "Trevor Challa", "age": 35}, - {"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}, - ] - ] -] - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.offset_and_limit import tutorial002_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_limit_and_offset/test_tutorial003.py b/tests/test_tutorial/test_limit_and_offset/test_tutorial003.py index 7192f7ef43..a33cf49f06 100644 --- a/tests/test_tutorial/test_limit_and_offset/test_tutorial003.py +++ b/tests/test_tutorial/test_limit_and_offset/test_tutorial003.py @@ -1,8 +1,25 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 + + +@pytest.fixture( + name="mod", + params=[ + "tutorial003", + pytest.param("tutorial003_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.offset_and_limit.{request.param}") + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + return mod + expected_calls = [ [ @@ -18,15 +35,6 @@ ] -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.offset_and_limit import tutorial003 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_limit_and_offset/test_tutorial003_py310.py b/tests/test_tutorial/test_limit_and_offset/test_tutorial003_py310.py deleted file mode 100644 index 993999156d..0000000000 --- a/tests/test_tutorial/test_limit_and_offset/test_tutorial003_py310.py +++ /dev/null @@ -1,33 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - [ - { - "id": 7, - "name": "Captain North America", - "secret_name": "Esteban Rogelios", - "age": 93, - } - ] - ] -] - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.offset_and_limit import tutorial003_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_limit_and_offset/test_tutorial004.py b/tests/test_tutorial/test_limit_and_offset/test_tutorial004.py index eb15a1560e..15322c4089 100644 --- a/tests/test_tutorial/test_limit_and_offset/test_tutorial004.py +++ b/tests/test_tutorial/test_limit_and_offset/test_tutorial004.py @@ -1,22 +1,29 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.offset_and_limit import tutorial004 as mod - +@pytest.fixture( + name="mod", + params=[ + "tutorial004", + pytest.param("tutorial004_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.offset_and_limit.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [ [ {"name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36, "id": 6}, diff --git a/tests/test_tutorial/test_limit_and_offset/test_tutorial004_py310.py b/tests/test_tutorial/test_limit_and_offset/test_tutorial004_py310.py deleted file mode 100644 index 4ca736589f..0000000000 --- a/tests/test_tutorial/test_limit_and_offset/test_tutorial004_py310.py +++ /dev/null @@ -1,27 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.offset_and_limit import tutorial004_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [ - [ - {"name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36, "id": 6}, - {"name": "Rusty-Man", "secret_name": "Tommy Sharp", "age": 48, "id": 3}, - ] - ] - ] diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial001.py b/tests/test_tutorial/test_many_to_many/test_tutorial001.py index 70bfe9a649..b84626d9cf 100644 --- a/tests/test_tutorial/test_many_to_many/test_tutorial001.py +++ b/tests/test_tutorial/test_many_to_many/test_tutorial001.py @@ -1,8 +1,26 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py39, needs_py310 + + +@pytest.fixture( + name="mod", + params=[ + "tutorial001", + pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.many_to_many.{request.param}") + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + return mod + expected_calls = [ [ @@ -35,15 +53,6 @@ ] -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.many_to_many import tutorial001 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial001_py310.py b/tests/test_tutorial/test_many_to_many/test_tutorial001_py310.py deleted file mode 100644 index bf31d9c695..0000000000 --- a/tests/test_tutorial/test_many_to_many/test_tutorial001_py310.py +++ /dev/null @@ -1,50 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Deadpond:", - {"id": 1, "secret_name": "Dive Wilson", "age": None, "name": "Deadpond"}, - ], - [ - "Deadpond teams:", - [ - {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret's Bar"}, - {"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}, - ], - ], - [ - "Rusty-Man:", - {"id": 2, "secret_name": "Tommy Sharp", "age": 48, "name": "Rusty-Man"}, - ], - [ - "Rusty-Man Teams:", - [{"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}], - ], - [ - "Spider-Boy:", - {"id": 3, "secret_name": "Pedro Parqueador", "age": None, "name": "Spider-Boy"}, - ], - [ - "Spider-Boy Teams:", - [{"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}], - ], -] - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.many_to_many import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial001_py39.py b/tests/test_tutorial/test_many_to_many/test_tutorial001_py39.py deleted file mode 100644 index cb7a4d8456..0000000000 --- a/tests/test_tutorial/test_many_to_many/test_tutorial001_py39.py +++ /dev/null @@ -1,50 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py39 - -expected_calls = [ - [ - "Deadpond:", - {"id": 1, "secret_name": "Dive Wilson", "age": None, "name": "Deadpond"}, - ], - [ - "Deadpond teams:", - [ - {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret's Bar"}, - {"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}, - ], - ], - [ - "Rusty-Man:", - {"id": 2, "secret_name": "Tommy Sharp", "age": 48, "name": "Rusty-Man"}, - ], - [ - "Rusty-Man Teams:", - [{"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}], - ], - [ - "Spider-Boy:", - {"id": 3, "secret_name": "Pedro Parqueador", "age": None, "name": "Spider-Boy"}, - ], - [ - "Spider-Boy Teams:", - [{"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}], - ], -] - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.many_to_many import tutorial001_py39 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial002.py b/tests/test_tutorial/test_many_to_many/test_tutorial002.py index d4d7d95e89..b5562f416d 100644 --- a/tests/test_tutorial/test_many_to_many/test_tutorial002.py +++ b/tests/test_tutorial/test_many_to_many/test_tutorial002.py @@ -1,8 +1,26 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py39, needs_py310 + + +@pytest.fixture( + name="mod", + params=[ + "tutorial002", + pytest.param("tutorial002_py39", marks=needs_py39), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.many_to_many.{request.param}") + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + return mod + expected_calls = [ [ @@ -62,15 +80,6 @@ ] -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.many_to_many import tutorial002 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial002_py310.py b/tests/test_tutorial/test_many_to_many/test_tutorial002_py310.py deleted file mode 100644 index ad7c892fcd..0000000000 --- a/tests/test_tutorial/test_many_to_many/test_tutorial002_py310.py +++ /dev/null @@ -1,77 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Deadpond:", - {"id": 1, "secret_name": "Dive Wilson", "age": None, "name": "Deadpond"}, - ], - [ - "Deadpond teams:", - [ - {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret's Bar"}, - {"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}, - ], - ], - [ - "Rusty-Man:", - {"id": 2, "secret_name": "Tommy Sharp", "age": 48, "name": "Rusty-Man"}, - ], - [ - "Rusty-Man Teams:", - [{"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}], - ], - [ - "Spider-Boy:", - {"id": 3, "secret_name": "Pedro Parqueador", "age": None, "name": "Spider-Boy"}, - ], - [ - "Spider-Boy Teams:", - [{"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}], - ], - [ - "Updated Spider-Boy's Teams:", - [ - {"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}, - {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret's Bar"}, - ], - ], - [ - "Z-Force heroes:", - [ - {"id": 1, "secret_name": "Dive Wilson", "age": None, "name": "Deadpond"}, - { - "id": 3, - "secret_name": "Pedro Parqueador", - "age": None, - "name": "Spider-Boy", - }, - ], - ], - [ - "Reverted Z-Force's heroes:", - [{"id": 1, "secret_name": "Dive Wilson", "age": None, "name": "Deadpond"}], - ], - [ - "Reverted Spider-Boy's teams:", - [{"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}], - ], -] - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.many_to_many import tutorial002_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial002_py39.py b/tests/test_tutorial/test_many_to_many/test_tutorial002_py39.py deleted file mode 100644 index c0df48d73c..0000000000 --- a/tests/test_tutorial/test_many_to_many/test_tutorial002_py39.py +++ /dev/null @@ -1,77 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py39 - -expected_calls = [ - [ - "Deadpond:", - {"id": 1, "secret_name": "Dive Wilson", "age": None, "name": "Deadpond"}, - ], - [ - "Deadpond teams:", - [ - {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret's Bar"}, - {"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}, - ], - ], - [ - "Rusty-Man:", - {"id": 2, "secret_name": "Tommy Sharp", "age": 48, "name": "Rusty-Man"}, - ], - [ - "Rusty-Man Teams:", - [{"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}], - ], - [ - "Spider-Boy:", - {"id": 3, "secret_name": "Pedro Parqueador", "age": None, "name": "Spider-Boy"}, - ], - [ - "Spider-Boy Teams:", - [{"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}], - ], - [ - "Updated Spider-Boy's Teams:", - [ - {"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}, - {"id": 1, "name": "Z-Force", "headquarters": "Sister Margaret's Bar"}, - ], - ], - [ - "Z-Force heroes:", - [ - {"id": 1, "secret_name": "Dive Wilson", "age": None, "name": "Deadpond"}, - { - "id": 3, - "secret_name": "Pedro Parqueador", - "age": None, - "name": "Spider-Boy", - }, - ], - ], - [ - "Reverted Z-Force's heroes:", - [{"id": 1, "secret_name": "Dive Wilson", "age": None, "name": "Deadpond"}], - ], - [ - "Reverted Spider-Boy's teams:", - [{"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}], - ], -] - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.many_to_many import tutorial002_py39 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial003.py b/tests/test_tutorial/test_many_to_many/test_tutorial003.py index 35489b01ce..ad1168e3ba 100644 --- a/tests/test_tutorial/test_many_to_many/test_tutorial003.py +++ b/tests/test_tutorial/test_many_to_many/test_tutorial003.py @@ -1,8 +1,26 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py39, needs_py310 + + +@pytest.fixture( + name="mod", + params=[ + "tutorial003", + pytest.param("tutorial003_py39", marks=needs_py39), + pytest.param("tutorial003_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.many_to_many.{request.param}") + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + return mod + expected_calls = [ [ @@ -58,15 +76,6 @@ ] -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.many_to_many import tutorial003 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial003_py310.py b/tests/test_tutorial/test_many_to_many/test_tutorial003_py310.py deleted file mode 100644 index 78a699c741..0000000000 --- a/tests/test_tutorial/test_many_to_many/test_tutorial003_py310.py +++ /dev/null @@ -1,73 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Z-Force hero:", - {"name": "Deadpond", "secret_name": "Dive Wilson", "id": 1, "age": None}, - "is training:", - False, - ], - [ - "Preventers hero:", - {"name": "Deadpond", "secret_name": "Dive Wilson", "id": 1, "age": None}, - "is training:", - True, - ], - [ - "Preventers hero:", - {"name": "Spider-Boy", "secret_name": "Pedro Parqueador", "id": 2, "age": None}, - "is training:", - True, - ], - [ - "Preventers hero:", - {"name": "Rusty-Man", "secret_name": "Tommy Sharp", "id": 3, "age": 48}, - "is training:", - False, - ], - [ - "Updated Spider-Boy's Teams:", - [ - {"team_id": 2, "is_training": True, "hero_id": 2}, - {"team_id": 1, "is_training": True, "hero_id": 2}, - ], - ], - [ - "Z-Force heroes:", - [ - {"team_id": 1, "is_training": False, "hero_id": 1}, - {"team_id": 1, "is_training": True, "hero_id": 2}, - ], - ], - [ - "Spider-Boy team:", - {"headquarters": "Sharp Tower", "id": 2, "name": "Preventers"}, - "is training:", - False, - ], - [ - "Spider-Boy team:", - {"headquarters": "Sister Margaret's Bar", "id": 1, "name": "Z-Force"}, - "is training:", - True, - ], -] - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.many_to_many import tutorial003_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial003_py39.py b/tests/test_tutorial/test_many_to_many/test_tutorial003_py39.py deleted file mode 100644 index 8fed921d82..0000000000 --- a/tests/test_tutorial/test_many_to_many/test_tutorial003_py39.py +++ /dev/null @@ -1,73 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py39 - -expected_calls = [ - [ - "Z-Force hero:", - {"name": "Deadpond", "secret_name": "Dive Wilson", "id": 1, "age": None}, - "is training:", - False, - ], - [ - "Preventers hero:", - {"name": "Deadpond", "secret_name": "Dive Wilson", "id": 1, "age": None}, - "is training:", - True, - ], - [ - "Preventers hero:", - {"name": "Spider-Boy", "secret_name": "Pedro Parqueador", "id": 2, "age": None}, - "is training:", - True, - ], - [ - "Preventers hero:", - {"name": "Rusty-Man", "secret_name": "Tommy Sharp", "id": 3, "age": 48}, - "is training:", - False, - ], - [ - "Updated Spider-Boy's Teams:", - [ - {"team_id": 2, "is_training": True, "hero_id": 2}, - {"team_id": 1, "is_training": True, "hero_id": 2}, - ], - ], - [ - "Z-Force heroes:", - [ - {"team_id": 1, "is_training": False, "hero_id": 1}, - {"team_id": 1, "is_training": True, "hero_id": 2}, - ], - ], - [ - "Spider-Boy team:", - {"headquarters": "Sharp Tower", "id": 2, "name": "Preventers"}, - "is training:", - False, - ], - [ - "Spider-Boy team:", - {"headquarters": "Sister Margaret's Bar", "id": 1, "name": "Z-Force"}, - "is training:", - True, - ], -] - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.many_to_many import tutorial003_py39 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_one/test_tutorial001.py b/tests/test_tutorial/test_one/test_tutorial001.py index deb133b985..2f6f1c9e89 100644 --- a/tests/test_tutorial/test_one/test_tutorial001.py +++ b/tests/test_tutorial/test_one/test_tutorial001.py @@ -1,22 +1,29 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial001 as mod - +@pytest.fixture( + name="mod", + params=[ + "tutorial001", + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.one.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [ "Hero:", { diff --git a/tests/test_tutorial/test_one/test_tutorial001_py310.py b/tests/test_tutorial/test_one/test_tutorial001_py310.py deleted file mode 100644 index 6de878087f..0000000000 --- a/tests/test_tutorial/test_one/test_tutorial001_py310.py +++ /dev/null @@ -1,30 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [ - "Hero:", - { - "name": "Tarantula", - "secret_name": "Natalia Roman-on", - "age": 32, - "id": 4, - }, - ] - ] diff --git a/tests/test_tutorial/test_one/test_tutorial002.py b/tests/test_tutorial/test_one/test_tutorial002.py index 7106564122..267ead8939 100644 --- a/tests/test_tutorial/test_one/test_tutorial002.py +++ b/tests/test_tutorial/test_one/test_tutorial002.py @@ -1,19 +1,26 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial002 as mod - +@pytest.fixture( + name="mod", + params=[ + "tutorial002", + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.one.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [["Hero:", None]] +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [["Hero:", None]] diff --git a/tests/test_tutorial/test_one/test_tutorial002_py310.py b/tests/test_tutorial/test_one/test_tutorial002_py310.py deleted file mode 100644 index afdfc54593..0000000000 --- a/tests/test_tutorial/test_one/test_tutorial002_py310.py +++ /dev/null @@ -1,20 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial002_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [["Hero:", None]] diff --git a/tests/test_tutorial/test_one/test_tutorial003.py b/tests/test_tutorial/test_one/test_tutorial003.py index 40a73d042b..de2e529aea 100644 --- a/tests/test_tutorial/test_one/test_tutorial003.py +++ b/tests/test_tutorial/test_one/test_tutorial003.py @@ -1,22 +1,29 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial003 as mod - +@pytest.fixture( + name="mod", + params=[ + "tutorial003", + pytest.param("tutorial003_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.one.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [ "Hero:", {"name": "Deadpond", "secret_name": "Dive Wilson", "age": None, "id": 1}, diff --git a/tests/test_tutorial/test_one/test_tutorial003_py310.py b/tests/test_tutorial/test_one/test_tutorial003_py310.py deleted file mode 100644 index 8eb8b8612b..0000000000 --- a/tests/test_tutorial/test_one/test_tutorial003_py310.py +++ /dev/null @@ -1,25 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial003_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [ - "Hero:", - {"name": "Deadpond", "secret_name": "Dive Wilson", "age": None, "id": 1}, - ] - ] diff --git a/tests/test_tutorial/test_one/test_tutorial004.py b/tests/test_tutorial/test_one/test_tutorial004.py index 5bd652577d..9df91fbb41 100644 --- a/tests/test_tutorial/test_one/test_tutorial004.py +++ b/tests/test_tutorial/test_one/test_tutorial004.py @@ -1,17 +1,28 @@ -from unittest.mock import patch +import importlib +from types import ModuleType import pytest from sqlalchemy.exc import MultipleResultsFound from sqlmodel import Session, create_engine, delete -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial004 as mod - +@pytest.fixture( + name="mod", + params=[ + "tutorial004", + pytest.param("tutorial004_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.one.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) + return mod + + +def test_tutorial(print_mock: PrintMock, mod: ModuleType): with pytest.raises(MultipleResultsFound): mod.main() with Session(mod.engine) as session: @@ -21,13 +32,8 @@ def test_tutorial(clear_sqlmodel): session.add(mod.Hero(name="Test Hero", secret_name="Secret Test Hero", age=24)) session.commit() - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.select_heroes() - assert calls == [ + mod.select_heroes() + assert print_mock.calls == [ [ "Hero:", { diff --git a/tests/test_tutorial/test_one/test_tutorial004_py310.py b/tests/test_tutorial/test_one/test_tutorial004_py310.py deleted file mode 100644 index cf365a4fe5..0000000000 --- a/tests/test_tutorial/test_one/test_tutorial004_py310.py +++ /dev/null @@ -1,41 +0,0 @@ -from unittest.mock import patch - -import pytest -from sqlalchemy.exc import MultipleResultsFound -from sqlmodel import Session, create_engine, delete - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial004_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - with pytest.raises(MultipleResultsFound): - mod.main() - with Session(mod.engine) as session: - # TODO: create delete() function - # TODO: add overloads for .exec() with delete object - session.exec(delete(mod.Hero)) - session.add(mod.Hero(name="Test Hero", secret_name="Secret Test Hero", age=24)) - session.commit() - - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.select_heroes() - assert calls == [ - [ - "Hero:", - { - "id": 1, - "name": "Test Hero", - "secret_name": "Secret Test Hero", - "age": 24, - }, - ] - ] diff --git a/tests/test_tutorial/test_one/test_tutorial005.py b/tests/test_tutorial/test_one/test_tutorial005.py index 0c25ffa39d..fa569020a1 100644 --- a/tests/test_tutorial/test_one/test_tutorial005.py +++ b/tests/test_tutorial/test_one/test_tutorial005.py @@ -1,17 +1,28 @@ -from unittest.mock import patch +import importlib +from types import ModuleType import pytest from sqlalchemy.exc import NoResultFound from sqlmodel import Session, create_engine, delete -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial005 as mod - +@pytest.fixture( + name="mod", + params=[ + "tutorial005", + pytest.param("tutorial005_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.one.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) + return mod + + +def test_tutorial(print_mock: PrintMock, mod: ModuleType): with pytest.raises(NoResultFound): mod.main() with Session(mod.engine) as session: @@ -21,13 +32,8 @@ def test_tutorial(clear_sqlmodel): session.add(mod.Hero(name="Test Hero", secret_name="Secret Test Hero", age=24)) session.commit() - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.select_heroes() - assert calls == [ + mod.select_heroes() + assert print_mock.calls == [ [ "Hero:", { diff --git a/tests/test_tutorial/test_one/test_tutorial005_py310.py b/tests/test_tutorial/test_one/test_tutorial005_py310.py deleted file mode 100644 index f1fce7d764..0000000000 --- a/tests/test_tutorial/test_one/test_tutorial005_py310.py +++ /dev/null @@ -1,41 +0,0 @@ -from unittest.mock import patch - -import pytest -from sqlalchemy.exc import NoResultFound -from sqlmodel import Session, create_engine, delete - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial005_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - with pytest.raises(NoResultFound): - mod.main() - with Session(mod.engine) as session: - # TODO: create delete() function - # TODO: add overloads for .exec() with delete object - session.exec(delete(mod.Hero)) - session.add(mod.Hero(name="Test Hero", secret_name="Secret Test Hero", age=24)) - session.commit() - - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.select_heroes() - assert calls == [ - [ - "Hero:", - { - "id": 1, - "name": "Test Hero", - "secret_name": "Secret Test Hero", - "age": 24, - }, - ] - ] diff --git a/tests/test_tutorial/test_one/test_tutorial006.py b/tests/test_tutorial/test_one/test_tutorial006.py index 01c1af4602..1c6c663dc5 100644 --- a/tests/test_tutorial/test_one/test_tutorial006.py +++ b/tests/test_tutorial/test_one/test_tutorial006.py @@ -1,22 +1,29 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial006 as mod - +@pytest.fixture( + name="mod", + params=[ + "tutorial006", + pytest.param("tutorial006_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.one.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [ "Hero:", {"name": "Deadpond", "secret_name": "Dive Wilson", "age": None, "id": 1}, diff --git a/tests/test_tutorial/test_one/test_tutorial006_py310.py b/tests/test_tutorial/test_one/test_tutorial006_py310.py deleted file mode 100644 index ad8577c7ae..0000000000 --- a/tests/test_tutorial/test_one/test_tutorial006_py310.py +++ /dev/null @@ -1,25 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial006_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [ - "Hero:", - {"name": "Deadpond", "secret_name": "Dive Wilson", "age": None, "id": 1}, - ] - ] diff --git a/tests/test_tutorial/test_one/test_tutorial007.py b/tests/test_tutorial/test_one/test_tutorial007.py index e8b984b050..ca4a5310a5 100644 --- a/tests/test_tutorial/test_one/test_tutorial007.py +++ b/tests/test_tutorial/test_one/test_tutorial007.py @@ -1,22 +1,29 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial007 as mod - +@pytest.fixture( + name="mod", + params=[ + "tutorial007", + pytest.param("tutorial007_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.one.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [ "Hero:", {"name": "Deadpond", "secret_name": "Dive Wilson", "age": None, "id": 1}, diff --git a/tests/test_tutorial/test_one/test_tutorial007_py310.py b/tests/test_tutorial/test_one/test_tutorial007_py310.py deleted file mode 100644 index 15b2306fc6..0000000000 --- a/tests/test_tutorial/test_one/test_tutorial007_py310.py +++ /dev/null @@ -1,25 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial007_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [ - "Hero:", - {"name": "Deadpond", "secret_name": "Dive Wilson", "age": None, "id": 1}, - ] - ] diff --git a/tests/test_tutorial/test_one/test_tutorial008.py b/tests/test_tutorial/test_one/test_tutorial008.py index e0ea766f37..da451fbaa2 100644 --- a/tests/test_tutorial/test_one/test_tutorial008.py +++ b/tests/test_tutorial/test_one/test_tutorial008.py @@ -1,22 +1,29 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial008 as mod - +@pytest.fixture( + name="mod", + params=[ + "tutorial008", + pytest.param("tutorial008_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.one.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [ "Hero:", {"name": "Deadpond", "secret_name": "Dive Wilson", "age": None, "id": 1}, diff --git a/tests/test_tutorial/test_one/test_tutorial008_py310.py b/tests/test_tutorial/test_one/test_tutorial008_py310.py deleted file mode 100644 index c7d1fe55c9..0000000000 --- a/tests/test_tutorial/test_one/test_tutorial008_py310.py +++ /dev/null @@ -1,25 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial008_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [ - "Hero:", - {"name": "Deadpond", "secret_name": "Dive Wilson", "age": None, "id": 1}, - ] - ] diff --git a/tests/test_tutorial/test_one/test_tutorial009.py b/tests/test_tutorial/test_one/test_tutorial009.py index 63e01fe741..0cf3a6267f 100644 --- a/tests/test_tutorial/test_one/test_tutorial009.py +++ b/tests/test_tutorial/test_one/test_tutorial009.py @@ -1,19 +1,26 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial009 as mod - +@pytest.fixture( + name="mod", + params=[ + "tutorial009", + pytest.param("tutorial009_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.one.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [["Hero:", None]] +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [["Hero:", None]] diff --git a/tests/test_tutorial/test_one/test_tutorial009_py310.py b/tests/test_tutorial/test_one/test_tutorial009_py310.py deleted file mode 100644 index 8e9fda5f73..0000000000 --- a/tests/test_tutorial/test_one/test_tutorial009_py310.py +++ /dev/null @@ -1,20 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.one import tutorial009_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [["Hero:", None]] diff --git a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001.py b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001.py index 30ec9fdc36..698a7af2cb 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001.py +++ b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001.py @@ -1,10 +1,29 @@ -from unittest.mock import patch +import importlib +from types import ModuleType import pytest from sqlalchemy.exc import SAWarning from sqlmodel import create_engine -from ....conftest import get_testing_print_function +from ....conftest import PrintMock, needs_py39, needs_py310 + + +@pytest.fixture( + name="mod", + params=[ + "tutorial001", + pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.relationship_attributes.back_populates.{request.param}" + ) + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + return mod + expected_calls = [ [ @@ -272,18 +291,7 @@ ] -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.back_populates import ( - tutorial001 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - with pytest.warns(SAWarning): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + with pytest.warns(SAWarning): + mod.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001_py310.py b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001_py310.py deleted file mode 100644 index 384056ad7b..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001_py310.py +++ /dev/null @@ -1,290 +0,0 @@ -from unittest.mock import patch - -import pytest -from sqlalchemy.exc import SAWarning -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "secret_name": "Dive Wilson", - "team_id": 1, - "name": "Deadpond", - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "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": 2, - "name": "Spider-Boy", - }, - ], - [ - "Team Wakaland:", - {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, - ], - [ - "Preventers new hero:", - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - ], - [ - "Preventers new hero:", - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - ], - [ - "Preventers new hero:", - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - [ - "Preventers heroes:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], - [ - "Hero Spider-Boy:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - ], - [ - "Preventers Team:", - {"headquarters": "Sharp Tower", "id": 2, "name": "Preventers"}, - ], - [ - "Preventers Team Heroes:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], - [ - "Spider-Boy without team:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - ], - [ - "Preventers Team Heroes again:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], - ["After committing"], - [ - "Spider-Boy after commit:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": None, - "name": "Spider-Boy", - }, - ], - [ - "Preventers Team Heroes after commit:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], -] - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.back_populates import ( - tutorial001_py310 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - with pytest.warns(SAWarning): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001_py39.py b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001_py39.py deleted file mode 100644 index 0597a88e89..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001_py39.py +++ /dev/null @@ -1,290 +0,0 @@ -from unittest.mock import patch - -import pytest -from sqlalchemy.exc import SAWarning -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py39 - -expected_calls = [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "secret_name": "Dive Wilson", - "team_id": 1, - "name": "Deadpond", - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "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": 2, - "name": "Spider-Boy", - }, - ], - [ - "Team Wakaland:", - {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, - ], - [ - "Preventers new hero:", - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - ], - [ - "Preventers new hero:", - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - ], - [ - "Preventers new hero:", - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - [ - "Preventers heroes:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], - [ - "Hero Spider-Boy:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - ], - [ - "Preventers Team:", - {"headquarters": "Sharp Tower", "id": 2, "name": "Preventers"}, - ], - [ - "Preventers Team Heroes:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], - [ - "Spider-Boy without team:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - ], - [ - "Preventers Team Heroes again:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], - ["After committing"], - [ - "Spider-Boy after commit:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": None, - "name": "Spider-Boy", - }, - ], - [ - "Preventers Team Heroes after commit:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], -] - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.back_populates import ( - tutorial001_py39 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - with pytest.warns(SAWarning): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002.py b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002.py index 98c01a9d54..8ce54be46c 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002.py +++ b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002.py @@ -1,8 +1,28 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ....conftest import get_testing_print_function +from ....conftest import PrintMock, needs_py39, needs_py310 + + +@pytest.fixture( + name="mod", + params=[ + "tutorial002", + pytest.param("tutorial002_py39", marks=needs_py39), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.relationship_attributes.back_populates.{request.param}" + ) + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + return mod + expected_calls = [ [ @@ -263,17 +283,6 @@ ] -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.back_populates import ( - tutorial002 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002_py310.py b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002_py310.py deleted file mode 100644 index 50a891f310..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002_py310.py +++ /dev/null @@ -1,280 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "secret_name": "Dive Wilson", - "team_id": 1, - "name": "Deadpond", - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "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": 2, - "name": "Spider-Boy", - }, - ], - [ - "Team Wakaland:", - {"id": 3, "name": "Wakaland", "headquarters": "Wakaland Capital City"}, - ], - [ - "Preventers new hero:", - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - ], - [ - "Preventers new hero:", - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - ], - [ - "Preventers new hero:", - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - [ - "Preventers heroes:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], - [ - "Hero Spider-Boy:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - ], - [ - "Preventers Team:", - {"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}, - ], - [ - "Preventers Team Heroes:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], - [ - "Spider-Boy without team:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - ], - [ - "Preventers Team Heroes again:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], - ["After committing"], - [ - "Spider-Boy after commit:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": None, - "name": "Spider-Boy", - }, - ], - [ - "Preventers Team Heroes after commit:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], -] - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.back_populates import ( - tutorial002_py310 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002_py39.py b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002_py39.py deleted file mode 100644 index 3da6ce4aac..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002_py39.py +++ /dev/null @@ -1,280 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py39 - -expected_calls = [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "secret_name": "Dive Wilson", - "team_id": 1, - "name": "Deadpond", - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "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": 2, - "name": "Spider-Boy", - }, - ], - [ - "Team Wakaland:", - {"id": 3, "name": "Wakaland", "headquarters": "Wakaland Capital City"}, - ], - [ - "Preventers new hero:", - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - ], - [ - "Preventers new hero:", - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - ], - [ - "Preventers new hero:", - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - [ - "Preventers heroes:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], - [ - "Hero Spider-Boy:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - ], - [ - "Preventers Team:", - {"id": 2, "name": "Preventers", "headquarters": "Sharp Tower"}, - ], - [ - "Preventers Team Heroes:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], - [ - "Spider-Boy without team:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - ], - [ - "Preventers Team Heroes again:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], - ["After committing"], - [ - "Spider-Boy after commit:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": None, - "name": "Spider-Boy", - }, - ], - [ - "Preventers Team Heroes after commit:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], -] - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.back_populates import ( - tutorial002_py39 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003.py b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003.py index 2ed66f76ca..d30c71c3c2 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003.py +++ b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003.py @@ -1,15 +1,32 @@ +import importlib +from types import ModuleType + +import pytest from sqlalchemy import inspect from sqlalchemy.engine.reflection import Inspector from sqlmodel import create_engine +from ....conftest import needs_py39, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.back_populates import ( - tutorial003 as mod, - ) +@pytest.fixture( + name="mod", + params=[ + "tutorial003", + pytest.param("tutorial003_py39", marks=needs_py39), + pytest.param("tutorial003_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.relationship_attributes.back_populates.{request.param}" + ) mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) + return mod + + +def test_tutorial(mod: ModuleType): mod.main() insp: Inspector = inspect(mod.engine) assert insp.has_table(str(mod.Hero.__tablename__)) diff --git a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003_py310.py b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003_py310.py deleted file mode 100644 index 82e0c1c03b..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003_py310.py +++ /dev/null @@ -1,21 +0,0 @@ -from sqlalchemy import inspect -from sqlalchemy.engine.reflection import Inspector -from sqlmodel import create_engine - -from ....conftest import needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.back_populates import ( - tutorial003_py310 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - mod.main() - insp: Inspector = inspect(mod.engine) - assert insp.has_table(str(mod.Hero.__tablename__)) - assert insp.has_table(str(mod.Weapon.__tablename__)) - assert insp.has_table(str(mod.Power.__tablename__)) - assert insp.has_table(str(mod.Team.__tablename__)) diff --git a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003_py39.py b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003_py39.py deleted file mode 100644 index d6059cb485..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003_py39.py +++ /dev/null @@ -1,21 +0,0 @@ -from sqlalchemy import inspect -from sqlalchemy.engine.reflection import Inspector -from sqlmodel import create_engine - -from ....conftest import needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.back_populates import ( - tutorial003_py39 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - mod.main() - insp: Inspector = inspect(mod.engine) - assert insp.has_table(str(mod.Hero.__tablename__)) - assert insp.has_table(str(mod.Weapon.__tablename__)) - assert insp.has_table(str(mod.Power.__tablename__)) - assert insp.has_table(str(mod.Team.__tablename__)) diff --git a/tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001.py b/tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001.py index 7ced57c835..0c6a229178 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001.py +++ b/tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001.py @@ -1,8 +1,28 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ....conftest import get_testing_print_function +from ....conftest import PrintMock, needs_py39, needs_py310 + + +@pytest.fixture( + name="mod", + params=[ + "tutorial001", + pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.relationship_attributes.create_and_update_relationships.{request.param}" + ) + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + return mod + expected_calls = [ [ @@ -82,17 +102,6 @@ ] -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.create_and_update_relationships import ( - tutorial001 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001_py310.py b/tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001_py310.py deleted file mode 100644 index c239b6d55c..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001_py310.py +++ /dev/null @@ -1,99 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "secret_name": "Dive Wilson", - "team_id": 1, - "name": "Deadpond", - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "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": 2, - "name": "Spider-Boy", - }, - ], - [ - "Team Wakaland:", - {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, - ], - [ - "Preventers new hero:", - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - ], - [ - "Preventers new hero:", - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - ], - [ - "Preventers new hero:", - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], -] - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.create_and_update_relationships import ( - tutorial001_py310 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001_py39.py b/tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001_py39.py deleted file mode 100644 index c569eed0d5..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001_py39.py +++ /dev/null @@ -1,99 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py39 - -expected_calls = [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "secret_name": "Dive Wilson", - "team_id": 1, - "name": "Deadpond", - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "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": 2, - "name": "Spider-Boy", - }, - ], - [ - "Team Wakaland:", - {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, - ], - [ - "Preventers new hero:", - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - ], - [ - "Preventers new hero:", - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - ], - [ - "Preventers new hero:", - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], -] - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.create_and_update_relationships import ( - tutorial001_py39 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001.py b/tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001.py index 14b38ca52e..96cf8bcc49 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001.py +++ b/tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001.py @@ -1,8 +1,28 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ....conftest import get_testing_print_function +from ....conftest import PrintMock, needs_py39, needs_py310 + + +@pytest.fixture( + name="mod", + params=[ + "tutorial001", + pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.relationship_attributes.define_relationship_attributes.{request.param}" + ) + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + return mod + expected_calls = [ [ @@ -38,17 +58,6 @@ ] -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.define_relationship_attributes import ( - tutorial001 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001_py310.py b/tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001_py310.py deleted file mode 100644 index f595dcaa04..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001_py310.py +++ /dev/null @@ -1,55 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Created hero:", - { - "name": "Deadpond", - "age": None, - "team_id": 1, - "id": 1, - "secret_name": "Dive Wilson", - }, - ], - [ - "Created hero:", - { - "name": "Rusty-Man", - "age": 48, - "team_id": 2, - "id": 2, - "secret_name": "Tommy Sharp", - }, - ], - [ - "Created hero:", - { - "name": "Spider-Boy", - "age": None, - "team_id": None, - "id": 3, - "secret_name": "Pedro Parqueador", - }, - ], -] - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.define_relationship_attributes import ( - tutorial001_py310 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001_py39.py b/tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001_py39.py deleted file mode 100644 index d54c610d19..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001_py39.py +++ /dev/null @@ -1,55 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py39 - -expected_calls = [ - [ - "Created hero:", - { - "name": "Deadpond", - "age": None, - "team_id": 1, - "id": 1, - "secret_name": "Dive Wilson", - }, - ], - [ - "Created hero:", - { - "name": "Rusty-Man", - "age": 48, - "team_id": 2, - "id": 2, - "secret_name": "Tommy Sharp", - }, - ], - [ - "Created hero:", - { - "name": "Spider-Boy", - "age": None, - "team_id": None, - "id": 3, - "secret_name": "Pedro Parqueador", - }, - ], -] - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.define_relationship_attributes import ( - tutorial001_py39 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001.py index 863a84eb1c..5e43f80c4c 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001.py +++ b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001.py @@ -1,24 +1,32 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ....conftest import get_testing_print_function +from ....conftest import PrintMock, needs_py39, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( - tutorial001 as mod, +@pytest.fixture( + name="mod", + params=[ + "tutorial001", + pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.relationship_attributes.cascade_delete_relationships.{request.param}" ) - mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [ "Created hero:", { diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001_py310.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001_py310.py deleted file mode 100644 index 3262d2b244..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001_py310.py +++ /dev/null @@ -1,73 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( - tutorial001_py310 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [ - "Created hero:", - { - "name": "Deadpond", - "secret_name": "Dive Wilson", - "team_id": 1, - "id": 1, - "age": None, - }, - ], - [ - "Created hero:", - { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "team_id": 2, - "id": 2, - "age": 48, - }, - ], - [ - "Created hero:", - { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": None, - "id": 3, - "age": None, - }, - ], - [ - "Updated hero:", - { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": 2, - "id": 3, - "age": None, - }, - ], - [ - "Team Wakaland:", - {"name": "Wakaland", "id": 3, "headquarters": "Wakaland Capital City"}, - ], - [ - "Deleted team:", - {"name": "Wakaland", "id": 3, "headquarters": "Wakaland Capital City"}, - ], - ["Black Lion not found:", None], - ["Princess Sure-E not found:", None], - ] diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001_py39.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001_py39.py deleted file mode 100644 index 840c354e83..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001_py39.py +++ /dev/null @@ -1,73 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( - tutorial001_py39 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [ - "Created hero:", - { - "name": "Deadpond", - "secret_name": "Dive Wilson", - "team_id": 1, - "id": 1, - "age": None, - }, - ], - [ - "Created hero:", - { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "team_id": 2, - "id": 2, - "age": 48, - }, - ], - [ - "Created hero:", - { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": None, - "id": 3, - "age": None, - }, - ], - [ - "Updated hero:", - { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": 2, - "id": 3, - "age": None, - }, - ], - [ - "Team Wakaland:", - {"name": "Wakaland", "id": 3, "headquarters": "Wakaland Capital City"}, - ], - [ - "Deleted team:", - {"name": "Wakaland", "id": 3, "headquarters": "Wakaland Capital City"}, - ], - ["Black Lion not found:", None], - ["Princess Sure-E not found:", None], - ] diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002.py index a7d7a26364..cb3907afd7 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002.py +++ b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002.py @@ -1,24 +1,32 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ....conftest import get_testing_print_function +from ....conftest import PrintMock, needs_py39, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( - tutorial002 as mod, +@pytest.fixture( + name="mod", + params=[ + "tutorial002", + pytest.param("tutorial002_py39", marks=needs_py39), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.relationship_attributes.cascade_delete_relationships.{request.param}" ) - mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [ "Created hero:", { diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002_py310.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002_py310.py deleted file mode 100644 index 5c755f3a29..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002_py310.py +++ /dev/null @@ -1,91 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( - tutorial002_py310 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "name": "Deadpond", - "secret_name": "Dive Wilson", - "team_id": 1, - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "team_id": 2, - }, - ], - [ - "Created hero:", - { - "age": None, - "id": 3, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": None, - }, - ], - [ - "Updated hero:", - { - "age": None, - "id": 3, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": 2, - }, - ], - [ - "Team Wakaland:", - {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, - ], - [ - "Deleted team:", - {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, - ], - [ - "Black Lion has no team:", - { - "age": 35, - "id": 4, - "name": "Black Lion", - "secret_name": "Trevor Challa", - "team_id": None, - }, - ], - [ - "Princess Sure-E has no team:", - { - "age": None, - "id": 5, - "name": "Princess Sure-E", - "secret_name": "Sure-E", - "team_id": None, - }, - ], - ] diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002_py39.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002_py39.py deleted file mode 100644 index 9937f6da4c..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002_py39.py +++ /dev/null @@ -1,91 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( - tutorial002_py39 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "name": "Deadpond", - "secret_name": "Dive Wilson", - "team_id": 1, - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "team_id": 2, - }, - ], - [ - "Created hero:", - { - "age": None, - "id": 3, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": None, - }, - ], - [ - "Updated hero:", - { - "age": None, - "id": 3, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": 2, - }, - ], - [ - "Team Wakaland:", - {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, - ], - [ - "Deleted team:", - {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, - ], - [ - "Black Lion has no team:", - { - "age": 35, - "id": 4, - "name": "Black Lion", - "secret_name": "Trevor Challa", - "team_id": None, - }, - ], - [ - "Princess Sure-E has no team:", - { - "age": None, - "id": 5, - "name": "Princess Sure-E", - "secret_name": "Sure-E", - "team_id": None, - }, - ], - ] diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003.py index a3d3bc0f05..e063630f96 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003.py +++ b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003.py @@ -1,24 +1,32 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from tests.conftest import get_testing_print_function +from ....conftest import PrintMock, needs_py39, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( - tutorial003 as mod, +@pytest.fixture( + name="mod", + params=[ + "tutorial003", + pytest.param("tutorial003_py39", marks=needs_py39), + pytest.param("tutorial003_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.relationship_attributes.cascade_delete_relationships.{request.param}" ) - mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [ "Created hero:", { diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003_py310.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003_py310.py deleted file mode 100644 index f9975f25f7..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003_py310.py +++ /dev/null @@ -1,91 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( - tutorial003_py310 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "name": "Deadpond", - "secret_name": "Dive Wilson", - "team_id": 1, - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "team_id": 2, - }, - ], - [ - "Created hero:", - { - "age": None, - "id": 3, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": None, - }, - ], - [ - "Updated hero:", - { - "age": None, - "id": 3, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": 2, - }, - ], - [ - "Team Wakaland:", - {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, - ], - [ - "Deleted team:", - {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, - ], - [ - "Black Lion has no team:", - { - "age": 35, - "id": 4, - "name": "Black Lion", - "secret_name": "Trevor Challa", - "team_id": None, - }, - ], - [ - "Princess Sure-E has no team:", - { - "age": None, - "id": 5, - "name": "Princess Sure-E", - "secret_name": "Sure-E", - "team_id": None, - }, - ], - ] diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003_py39.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003_py39.py deleted file mode 100644 index b68bc6237d..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003_py39.py +++ /dev/null @@ -1,91 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( - tutorial003_py39 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "name": "Deadpond", - "secret_name": "Dive Wilson", - "team_id": 1, - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "team_id": 2, - }, - ], - [ - "Created hero:", - { - "age": None, - "id": 3, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": None, - }, - ], - [ - "Updated hero:", - { - "age": None, - "id": 3, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": 2, - }, - ], - [ - "Team Wakaland:", - {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, - ], - [ - "Deleted team:", - {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, - ], - [ - "Black Lion has no team:", - { - "age": 35, - "id": 4, - "name": "Black Lion", - "secret_name": "Trevor Challa", - "team_id": None, - }, - ], - [ - "Princess Sure-E has no team:", - { - "age": None, - "id": 5, - "name": "Princess Sure-E", - "secret_name": "Sure-E", - "team_id": None, - }, - ], - ] diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004.py index d5da12e6a5..8b6c101fb8 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004.py +++ b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004.py @@ -1,36 +1,41 @@ -from unittest.mock import patch +import importlib +from types import ModuleType import pytest from sqlalchemy.exc import IntegrityError from sqlmodel import Session, create_engine, select -from tests.conftest import get_testing_print_function +from ....conftest import PrintMock, needs_py39, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( - tutorial004 as mod, +@pytest.fixture( + name="mod", + params=[ + "tutorial004", + pytest.param("tutorial004_py39", marks=needs_py39), + pytest.param("tutorial004_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.relationship_attributes.cascade_delete_relationships.{request.param}" ) - mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.create_db_and_tables() - mod.create_heroes() - mod.select_deleted_heroes() - with Session(mod.engine) as session: - team = session.exec( - select(mod.Team).where(mod.Team.name == "Wakaland") - ).one() - team.heroes.clear() - session.add(team) - session.commit() - mod.delete_team() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.create_db_and_tables() + mod.create_heroes() + mod.select_deleted_heroes() + with Session(mod.engine) as session: + team = session.exec(select(mod.Team).where(mod.Team.name == "Wakaland")).one() + team.heroes.clear() + session.add(team) + session.commit() + mod.delete_team() + assert print_mock.calls == [ [ "Created hero:", { diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004_py310.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004_py310.py deleted file mode 100644 index 3ce37700cf..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004_py310.py +++ /dev/null @@ -1,107 +0,0 @@ -from unittest.mock import patch - -import pytest -from sqlalchemy.exc import IntegrityError -from sqlmodel import Session, create_engine, select - -from tests.conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( - tutorial004_py310 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.create_db_and_tables() - mod.create_heroes() - mod.select_deleted_heroes() - with Session(mod.engine) as session: - team = session.exec( - select(mod.Team).where(mod.Team.name == "Wakaland") - ).one() - team.heroes.clear() - session.add(team) - session.commit() - mod.delete_team() - assert calls == [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "name": "Deadpond", - "secret_name": "Dive Wilson", - "team_id": 1, - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "team_id": 2, - }, - ], - [ - "Created hero:", - { - "age": None, - "id": 3, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": None, - }, - ], - [ - "Updated hero:", - { - "age": None, - "id": 3, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": 2, - }, - ], - [ - "Team Wakaland:", - {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, - ], - [ - "Black Lion has no team:", - { - "age": 35, - "id": 4, - "name": "Black Lion", - "secret_name": "Trevor Challa", - "team_id": 3, - }, - ], - [ - "Princess Sure-E has no team:", - { - "age": None, - "id": 5, - "name": "Princess Sure-E", - "secret_name": "Sure-E", - "team_id": 3, - }, - ], - [ - "Deleted team:", - {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, - ], - ] - - with pytest.raises(IntegrityError) as exc: - mod.main() - assert "FOREIGN KEY constraint failed" in str(exc.value) diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004_py39.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004_py39.py deleted file mode 100644 index 1c51fc0c90..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004_py39.py +++ /dev/null @@ -1,107 +0,0 @@ -from unittest.mock import patch - -import pytest -from sqlalchemy.exc import IntegrityError -from sqlmodel import Session, create_engine, select - -from tests.conftest import get_testing_print_function, needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( - tutorial004_py39 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.create_db_and_tables() - mod.create_heroes() - mod.select_deleted_heroes() - with Session(mod.engine) as session: - team = session.exec( - select(mod.Team).where(mod.Team.name == "Wakaland") - ).one() - team.heroes.clear() - session.add(team) - session.commit() - mod.delete_team() - assert calls == [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "name": "Deadpond", - "secret_name": "Dive Wilson", - "team_id": 1, - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "team_id": 2, - }, - ], - [ - "Created hero:", - { - "age": None, - "id": 3, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": None, - }, - ], - [ - "Updated hero:", - { - "age": None, - "id": 3, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": 2, - }, - ], - [ - "Team Wakaland:", - {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, - ], - [ - "Black Lion has no team:", - { - "age": 35, - "id": 4, - "name": "Black Lion", - "secret_name": "Trevor Challa", - "team_id": 3, - }, - ], - [ - "Princess Sure-E has no team:", - { - "age": None, - "id": 5, - "name": "Princess Sure-E", - "secret_name": "Sure-E", - "team_id": 3, - }, - ], - [ - "Deleted team:", - {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, - ], - ] - - with pytest.raises(IntegrityError) as exc: - mod.main() - assert "FOREIGN KEY constraint failed" in str(exc.value) diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005.py index a6a00608a9..273007770b 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005.py +++ b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005.py @@ -1,24 +1,32 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from tests.conftest import get_testing_print_function +from ....conftest import PrintMock, needs_py39, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( - tutorial005 as mod, +@pytest.fixture( + name="mod", + params=[ + "tutorial005", + pytest.param("tutorial005_py39", marks=needs_py39), + pytest.param("tutorial005_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.relationship_attributes.cascade_delete_relationships.{request.param}" ) - mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [ "Created hero:", { diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005_py310.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005_py310.py deleted file mode 100644 index 54ad1b79de..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005_py310.py +++ /dev/null @@ -1,95 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from tests.conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( - tutorial005_py310 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [ - "Created hero:", - { - "name": "Deadpond", - "secret_name": "Dive Wilson", - "team_id": 1, - "id": 1, - "age": None, - }, - ], - [ - "Created hero:", - { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "team_id": 2, - "id": 2, - "age": 48, - }, - ], - [ - "Created hero:", - { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": None, - "id": 3, - "age": None, - }, - ], - [ - "Updated hero:", - { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": 2, - "id": 3, - "age": None, - }, - ], - [ - "Team Wakaland:", - {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, - ], - [ - "Team with removed heroes:", - {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, - ], - [ - "Deleted team:", - {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, - ], - [ - "Black Lion has no team:", - { - "name": "Black Lion", - "secret_name": "Trevor Challa", - "team_id": None, - "id": 4, - "age": 35, - }, - ], - [ - "Princess Sure-E has no team:", - { - "name": "Princess Sure-E", - "secret_name": "Sure-E", - "team_id": None, - "id": 5, - "age": None, - }, - ], - ] diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005_py39.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005_py39.py deleted file mode 100644 index 8151ab9232..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005_py39.py +++ /dev/null @@ -1,95 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from tests.conftest import get_testing_print_function, needs_py39 - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.cascade_delete_relationships import ( - tutorial005_py39 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [ - "Created hero:", - { - "name": "Deadpond", - "secret_name": "Dive Wilson", - "team_id": 1, - "id": 1, - "age": None, - }, - ], - [ - "Created hero:", - { - "name": "Rusty-Man", - "secret_name": "Tommy Sharp", - "team_id": 2, - "id": 2, - "age": 48, - }, - ], - [ - "Created hero:", - { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": None, - "id": 3, - "age": None, - }, - ], - [ - "Updated hero:", - { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "team_id": 2, - "id": 3, - "age": None, - }, - ], - [ - "Team Wakaland:", - {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, - ], - [ - "Team with removed heroes:", - {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, - ], - [ - "Deleted team:", - {"id": 3, "headquarters": "Wakaland Capital City", "name": "Wakaland"}, - ], - [ - "Black Lion has no team:", - { - "name": "Black Lion", - "secret_name": "Trevor Challa", - "team_id": None, - "id": 4, - "age": 35, - }, - ], - [ - "Princess Sure-E has no team:", - { - "name": "Princess Sure-E", - "secret_name": "Sure-E", - "team_id": None, - "id": 5, - "age": None, - }, - ], - ] diff --git a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001.py b/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001.py index 9fc70012d8..1b2caaa9c5 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001.py +++ b/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001.py @@ -1,8 +1,28 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ....conftest import get_testing_print_function +from ....conftest import PrintMock, needs_py39, needs_py310 + + +@pytest.fixture( + name="mod", + params=[ + "tutorial001", + pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.relationship_attributes.read_relationships.{request.param}" + ) + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + return mod + expected_calls = [ [ @@ -90,17 +110,6 @@ ] -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.read_relationships import ( - tutorial001 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001_py310.py b/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001_py310.py deleted file mode 100644 index 9a4e3cc53b..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001_py310.py +++ /dev/null @@ -1,107 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "secret_name": "Dive Wilson", - "team_id": 1, - "name": "Deadpond", - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "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": 2, - "name": "Spider-Boy", - }, - ], - [ - "Team Wakaland:", - {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, - ], - [ - "Preventers new hero:", - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - ], - [ - "Preventers new hero:", - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - ], - [ - "Preventers new hero:", - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - [ - "Spider-Boy's team:", - {"headquarters": "Sharp Tower", "id": 2, "name": "Preventers"}, - ], - [ - "Spider-Boy's team again:", - {"headquarters": "Sharp Tower", "id": 2, "name": "Preventers"}, - ], -] - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.read_relationships import ( - tutorial001_py310 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001_py39.py b/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001_py39.py deleted file mode 100644 index 6b23980665..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001_py39.py +++ /dev/null @@ -1,107 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py39 - -expected_calls = [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "secret_name": "Dive Wilson", - "team_id": 1, - "name": "Deadpond", - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "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": 2, - "name": "Spider-Boy", - }, - ], - [ - "Team Wakaland:", - {"headquarters": "Wakaland Capital City", "id": 3, "name": "Wakaland"}, - ], - [ - "Preventers new hero:", - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - ], - [ - "Preventers new hero:", - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - ], - [ - "Preventers new hero:", - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - [ - "Spider-Boy's team:", - {"headquarters": "Sharp Tower", "id": 2, "name": "Preventers"}, - ], - [ - "Spider-Boy's team again:", - {"headquarters": "Sharp Tower", "id": 2, "name": "Preventers"}, - ], -] - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.read_relationships import ( - tutorial001_py39 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002.py b/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002.py index d827b1ff15..68ee2cc1d8 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002.py +++ b/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002.py @@ -1,8 +1,28 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ....conftest import get_testing_print_function +from ....conftest import PrintMock, needs_py39, needs_py310 + + +@pytest.fixture( + name="mod", + params=[ + "tutorial002", + pytest.param("tutorial002_py39", marks=needs_py39), + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module( + f"docs_src.tutorial.relationship_attributes.read_relationships.{request.param}" + ) + mod.sqlite_url = "sqlite://" + mod.engine = create_engine(mod.sqlite_url) + return mod + expected_calls = [ [ @@ -132,17 +152,6 @@ ] -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.read_relationships import ( - tutorial002 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002_py310.py b/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002_py310.py deleted file mode 100644 index 0cc9ae3326..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002_py310.py +++ /dev/null @@ -1,149 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "secret_name": "Dive Wilson", - "team_id": 1, - "name": "Deadpond", - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "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": 2, - "name": "Spider-Boy", - }, - ], - [ - "Team Wakaland:", - {"id": 3, "name": "Wakaland", "headquarters": "Wakaland Capital City"}, - ], - [ - "Preventers new hero:", - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - ], - [ - "Preventers new hero:", - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - ], - [ - "Preventers new hero:", - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - [ - "Preventers heroes:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], - [ - "Spider-Boy without team:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": None, - "name": "Spider-Boy", - }, - ], -] - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.read_relationships import ( - tutorial002_py310 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002_py39.py b/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002_py39.py deleted file mode 100644 index 891f4ca6a9..0000000000 --- a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002_py39.py +++ /dev/null @@ -1,149 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ....conftest import get_testing_print_function, needs_py39 - -expected_calls = [ - [ - "Created hero:", - { - "age": None, - "id": 1, - "secret_name": "Dive Wilson", - "team_id": 1, - "name": "Deadpond", - }, - ], - [ - "Created hero:", - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "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": 2, - "name": "Spider-Boy", - }, - ], - [ - "Team Wakaland:", - {"id": 3, "name": "Wakaland", "headquarters": "Wakaland Capital City"}, - ], - [ - "Preventers new hero:", - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - ], - [ - "Preventers new hero:", - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - ], - [ - "Preventers new hero:", - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - [ - "Preventers heroes:", - [ - { - "age": 48, - "id": 2, - "secret_name": "Tommy Sharp", - "team_id": 2, - "name": "Rusty-Man", - }, - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": 2, - "name": "Spider-Boy", - }, - { - "age": 32, - "id": 6, - "secret_name": "Natalia Roman-on", - "team_id": 2, - "name": "Tarantula", - }, - { - "age": 36, - "id": 7, - "secret_name": "Steve Weird", - "team_id": 2, - "name": "Dr. Weird", - }, - { - "age": 93, - "id": 8, - "secret_name": "Esteban Rogelios", - "team_id": 2, - "name": "Captain North America", - }, - ], - ], - [ - "Spider-Boy without team:", - { - "age": None, - "id": 3, - "secret_name": "Pedro Parqueador", - "team_id": None, - "name": "Spider-Boy", - }, - ], -] - - -@needs_py39 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.relationship_attributes.read_relationships import ( - tutorial002_py39 as mod, - ) - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_select/test_tutorial001_py310_tutorial002_py310.py b/tests/test_tutorial/test_select/test_tutorial001_py310_tutorial002_py310.py deleted file mode 100644 index 7521b6b717..0000000000 --- a/tests/test_tutorial/test_select/test_tutorial001_py310_tutorial002_py310.py +++ /dev/null @@ -1,57 +0,0 @@ -from typing import Any, Dict, List, Union -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -def check_calls(calls: List[List[Union[str, Dict[str, Any]]]]): - 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, - } - - -@needs_py310 -def test_tutorial_001(clear_sqlmodel): - from docs_src.tutorial.select import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - check_calls(calls) - - -@needs_py310 -def test_tutorial_002(clear_sqlmodel): - from docs_src.tutorial.select import tutorial002_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - check_calls(calls) diff --git a/tests/test_tutorial/test_select/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_select/test_tutorial001_tutorial002.py index fc8d546a19..a3f5e90d46 100644 --- a/tests/test_tutorial/test_select/test_tutorial001_tutorial002.py +++ b/tests/test_tutorial/test_select/test_tutorial001_tutorial002.py @@ -1,9 +1,11 @@ +import importlib +from types import ModuleType from typing import Any, Dict, List, Union -from unittest.mock import patch +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 def check_calls(calls: List[List[Union[str, Dict[str, Any]]]]): @@ -27,29 +29,35 @@ def check_calls(calls: List[List[Union[str, Dict[str, Any]]]]): } -def test_tutorial_001(clear_sqlmodel): - from docs_src.tutorial.select import tutorial001 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - check_calls(calls) - - -def test_tutorial_002(clear_sqlmodel): - from docs_src.tutorial.select import tutorial002 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - check_calls(calls) +@pytest.fixture(name="module") +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module(f"docs_src.tutorial.select.{request.param}") + module.sqlite_url = "sqlite://" + module.engine = create_engine(module.sqlite_url) + return module + + +@pytest.mark.parametrize( + "module", + [ + "tutorial001", + pytest.param("tutorial001_py310", marks=needs_py310), + ], + indirect=True, +) +def test_tutorial_001(print_mock: PrintMock, module: ModuleType): + module.main() + check_calls(print_mock.calls) + + +@pytest.mark.parametrize( + "module", + [ + "tutorial002", + pytest.param("tutorial002_py310", marks=needs_py310), + ], + indirect=True, +) +def test_tutorial_002(print_mock: PrintMock, module: ModuleType): + module.main() + check_calls(print_mock.calls) diff --git a/tests/test_tutorial/test_select/test_tutorial003_py310_tutorial004_py310.py b/tests/test_tutorial/test_select/test_tutorial003_py310_tutorial004_py310.py deleted file mode 100644 index 0fa69df4a1..0000000000 --- a/tests/test_tutorial/test_select/test_tutorial003_py310_tutorial004_py310.py +++ /dev/null @@ -1,59 +0,0 @@ -from typing import Any, Dict, List, Union -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -def check_calls(calls: List[List[Union[str, Dict[str, Any]]]]): - assert calls[0][0] == [ - { - "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, - }, - ] - - -@needs_py310 -def test_tutorial_003(clear_sqlmodel): - from docs_src.tutorial.select import tutorial003_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - check_calls(calls) - - -@needs_py310 -def test_tutorial_002(clear_sqlmodel): - from docs_src.tutorial.select import tutorial004_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - check_calls(calls) diff --git a/tests/test_tutorial/test_select/test_tutorial003_tutorial004.py b/tests/test_tutorial/test_select/test_tutorial003_tutorial004.py index bfda0cb189..967ecdbd50 100644 --- a/tests/test_tutorial/test_select/test_tutorial003_tutorial004.py +++ b/tests/test_tutorial/test_select/test_tutorial003_tutorial004.py @@ -1,9 +1,11 @@ +import importlib +from types import ModuleType from typing import Any, Dict, List, Union -from unittest.mock import patch +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 def check_calls(calls: List[List[Union[str, Dict[str, Any]]]]): @@ -29,29 +31,35 @@ def check_calls(calls: List[List[Union[str, Dict[str, Any]]]]): ] -def test_tutorial_003(clear_sqlmodel): - from docs_src.tutorial.select import tutorial003 as mod +@pytest.fixture(name="module") +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module(f"docs_src.tutorial.select.{request.param}") + module.sqlite_url = "sqlite://" + module.engine = create_engine(module.sqlite_url) + return module - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - new_print = get_testing_print_function(calls) +@pytest.mark.parametrize( + "module", + [ + "tutorial003", + pytest.param("tutorial003_py310", marks=needs_py310), + ], + indirect=True, +) +def test_tutorial_003(print_mock: PrintMock, module: ModuleType): + module.main() + check_calls(print_mock.calls) - with patch("builtins.print", new=new_print): - mod.main() - check_calls(calls) - -def test_tutorial_002(clear_sqlmodel): - from docs_src.tutorial.select import tutorial004 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - check_calls(calls) +@pytest.mark.parametrize( + "module", + [ + "tutorial004", + pytest.param("tutorial004_py310", marks=needs_py310), + ], + indirect=True, +) +def test_tutorial_004(print_mock: PrintMock, module: ModuleType): + module.main() + check_calls(print_mock.calls) diff --git a/tests/test_tutorial/test_update/test_tutorial001_py310_tutorial002_py310.py b/tests/test_tutorial/test_update/test_tutorial001_py310_tutorial002_py310.py deleted file mode 100644 index cefb75f333..0000000000 --- a/tests/test_tutorial/test_update/test_tutorial001_py310_tutorial002_py310.py +++ /dev/null @@ -1,56 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Hero:", - { - "id": 2, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "age": None, - }, - ], - [ - "Updated hero:", - { - "id": 2, - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "age": 16, - }, - ], -] - - -@needs_py310 -def test_tutorial001(clear_sqlmodel): - from docs_src.tutorial.update import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls - - -@needs_py310 -def test_tutorial002(clear_sqlmodel): - from docs_src.tutorial.update import tutorial002_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_update/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_update/test_tutorial001_tutorial002.py index be81f410bf..ef12a8d950 100644 --- a/tests/test_tutorial/test_update/test_tutorial001_tutorial002.py +++ b/tests/test_tutorial/test_update/test_tutorial001_tutorial002.py @@ -1,8 +1,10 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 expected_calls = [ [ @@ -26,29 +28,35 @@ ] -def test_tutorial001(clear_sqlmodel): - from docs_src.tutorial.update import tutorial001 as mod +@pytest.fixture(name="module") +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module(f"docs_src.tutorial.update.{request.param}") + module.sqlite_url = "sqlite://" + module.engine = create_engine(module.sqlite_url) + return module - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls - - -def test_tutorial002(clear_sqlmodel): - from docs_src.tutorial.update import tutorial002 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] +@pytest.mark.parametrize( + "module", + [ + "tutorial001", + pytest.param("tutorial001_py310", marks=needs_py310), + ], + indirect=True, +) +def test_tutorial001(print_mock: PrintMock, module: ModuleType): + module.main() + assert print_mock.calls == expected_calls - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +@pytest.mark.parametrize( + "module", + [ + "tutorial002", + pytest.param("tutorial002_py310", marks=needs_py310), + ], + indirect=True, +) +def test_tutorial002(print_mock: PrintMock, module: ModuleType): + module.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_update/test_tutorial003_py310_tutorial004_py310.py b/tests/test_tutorial/test_update/test_tutorial003_py310_tutorial004_py310.py deleted file mode 100644 index 31dc601901..0000000000 --- a/tests/test_tutorial/test_update/test_tutorial003_py310_tutorial004_py310.py +++ /dev/null @@ -1,69 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - -expected_calls = [ - [ - "Hero 1:", - {"id": 2, "name": "Spider-Boy", "secret_name": "Pedro Parqueador", "age": None}, - ], - [ - "Hero 2:", - { - "id": 7, - "name": "Captain North America", - "secret_name": "Esteban Rogelios", - "age": 93, - }, - ], - [ - "Updated hero 1:", - { - "id": 2, - "name": "Spider-Youngster", - "secret_name": "Pedro Parqueador", - "age": 16, - }, - ], - [ - "Updated hero 2:", - { - "id": 7, - "name": "Captain North America Except Canada", - "secret_name": "Esteban Rogelios", - "age": 110, - }, - ], -] - - -@needs_py310 -def test_tutorial003(clear_sqlmodel): - from docs_src.tutorial.update import tutorial003_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls - - -@needs_py310 -def test_tutorial004(clear_sqlmodel): - from docs_src.tutorial.update import tutorial004_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls diff --git a/tests/test_tutorial/test_update/test_tutorial003_tutorial004.py b/tests/test_tutorial/test_update/test_tutorial003_tutorial004.py index 0f705aa699..76788c62cd 100644 --- a/tests/test_tutorial/test_update/test_tutorial003_tutorial004.py +++ b/tests/test_tutorial/test_update/test_tutorial003_tutorial004.py @@ -1,8 +1,10 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 expected_calls = [ [ @@ -39,29 +41,35 @@ ] -def test_tutorial003(clear_sqlmodel): - from docs_src.tutorial.update import tutorial003 as mod +@pytest.fixture(name="module") +def get_module(request: pytest.FixtureRequest) -> ModuleType: + module = importlib.import_module(f"docs_src.tutorial.update.{request.param}") + module.sqlite_url = "sqlite://" + module.engine = create_engine(module.sqlite_url) + return module - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls - - -def test_tutorial004(clear_sqlmodel): - from docs_src.tutorial.update import tutorial004 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] +@pytest.mark.parametrize( + "module", + [ + "tutorial003", + pytest.param("tutorial003_py310", marks=needs_py310), + ], + indirect=True, +) +def test_tutorial003(print_mock: PrintMock, module: ModuleType): + module.main() + assert print_mock.calls == expected_calls - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == expected_calls +@pytest.mark.parametrize( + "module", + [ + "tutorial004", + pytest.param("tutorial004_py310", marks=needs_py310), + ], + indirect=True, +) +def test_tutorial004(print_mock: PrintMock, module: ModuleType): + module.main() + assert print_mock.calls == expected_calls diff --git a/tests/test_tutorial/test_where/test_tutorial001.py b/tests/test_tutorial/test_where/test_tutorial001.py index bba13269a1..1a557deefc 100644 --- a/tests/test_tutorial/test_where/test_tutorial001.py +++ b/tests/test_tutorial/test_where/test_tutorial001.py @@ -1,22 +1,29 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial001 as mod - +@pytest.fixture( + name="mod", + params=[ + "tutorial001", + pytest.param("tutorial001_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.where.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [ { "name": "Deadpond", diff --git a/tests/test_tutorial/test_where/test_tutorial001_py310.py b/tests/test_tutorial/test_where/test_tutorial001_py310.py deleted file mode 100644 index 44e734ad7d..0000000000 --- a/tests/test_tutorial/test_where/test_tutorial001_py310.py +++ /dev/null @@ -1,29 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial001_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [ - { - "name": "Deadpond", - "secret_name": "Dive Wilson", - "age": None, - "id": 1, - } - ] - ] diff --git a/tests/test_tutorial/test_where/test_tutorial002.py b/tests/test_tutorial/test_where/test_tutorial002.py index 80d60ff555..1c96f76126 100644 --- a/tests/test_tutorial/test_where/test_tutorial002.py +++ b/tests/test_tutorial/test_where/test_tutorial002.py @@ -1,22 +1,29 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial002 as mod - +@pytest.fixture( + name="mod", + params=[ + "tutorial002", + pytest.param("tutorial002_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.where.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [ { "name": "Spider-Boy", diff --git a/tests/test_tutorial/test_where/test_tutorial002_py310.py b/tests/test_tutorial/test_where/test_tutorial002_py310.py deleted file mode 100644 index 00d88ecdde..0000000000 --- a/tests/test_tutorial/test_where/test_tutorial002_py310.py +++ /dev/null @@ -1,30 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial002_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [ - { - "name": "Spider-Boy", - "secret_name": "Pedro Parqueador", - "age": None, - "id": 2, - } - ], - [{"name": "Rusty-Man", "secret_name": "Tommy Sharp", "age": 48, "id": 3}], - ] diff --git a/tests/test_tutorial/test_where/test_tutorial003.py b/tests/test_tutorial/test_where/test_tutorial003.py index 4794d846ff..6e90d22fc4 100644 --- a/tests/test_tutorial/test_where/test_tutorial003.py +++ b/tests/test_tutorial/test_where/test_tutorial003.py @@ -1,21 +1,28 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial003 as mod - +@pytest.fixture( + name="mod", + params=[ + "tutorial003", + pytest.param("tutorial003_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.where.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() expected_calls = [ [{"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}], @@ -29,6 +36,7 @@ def test_tutorial(clear_sqlmodel): } ], ] + calls = print_mock.calls for call in expected_calls: assert call in calls, "This expected item should be in the list" # Now that this item was checked, remove it from the list diff --git a/tests/test_tutorial/test_where/test_tutorial003_py310.py b/tests/test_tutorial/test_where/test_tutorial003_py310.py deleted file mode 100644 index 2d84c2ca82..0000000000 --- a/tests/test_tutorial/test_where/test_tutorial003_py310.py +++ /dev/null @@ -1,37 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial003_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - - expected_calls = [ - [{"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}], - [{"id": 3, "name": "Rusty-Man", "secret_name": "Tommy Sharp", "age": 48}], - [ - { - "id": 7, - "name": "Captain North America", - "secret_name": "Esteban Rogelios", - "age": 93, - } - ], - ] - for call in expected_calls: - assert call in calls, "This expected item should be in the list" - # Now that this item was checked, remove it from the list - calls.pop(calls.index(call)) - assert len(calls) == 0, "The list should only have the expected items" diff --git a/tests/test_tutorial/test_where/test_tutorial004.py b/tests/test_tutorial/test_where/test_tutorial004.py index 682babd43a..b7a1212b77 100644 --- a/tests/test_tutorial/test_where/test_tutorial004.py +++ b/tests/test_tutorial/test_where/test_tutorial004.py @@ -1,21 +1,28 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial004 as mod - +@pytest.fixture( + name="mod", + params=[ + "tutorial004", + pytest.param("tutorial004_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.where.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() expected_calls = [ [{"id": 5, "name": "Black Lion", "secret_name": "Trevor Challa", "age": 35}], [{"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}], @@ -29,6 +36,7 @@ def test_tutorial(clear_sqlmodel): } ], ] + calls = print_mock.calls for call in expected_calls: assert call in calls, "This expected item should be in the list" # Now that this item was checked, remove it from the list diff --git a/tests/test_tutorial/test_where/test_tutorial004_py310.py b/tests/test_tutorial/test_where/test_tutorial004_py310.py deleted file mode 100644 index 04566cbbec..0000000000 --- a/tests/test_tutorial/test_where/test_tutorial004_py310.py +++ /dev/null @@ -1,37 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial004_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - expected_calls = [ - [{"id": 5, "name": "Black Lion", "secret_name": "Trevor Challa", "age": 35}], - [{"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}], - [{"id": 3, "name": "Rusty-Man", "secret_name": "Tommy Sharp", "age": 48}], - [ - { - "id": 7, - "name": "Captain North America", - "secret_name": "Esteban Rogelios", - "age": 93, - } - ], - ] - for call in expected_calls: - assert call in calls, "This expected item should be in the list" - # Now that this item was checked, remove it from the list - calls.pop(calls.index(call)) - assert len(calls) == 0, "The list should only have the expected items" diff --git a/tests/test_tutorial/test_where/test_tutorial005.py b/tests/test_tutorial/test_where/test_tutorial005.py index b6bfd2ce88..9adbec74a2 100644 --- a/tests/test_tutorial/test_where/test_tutorial005.py +++ b/tests/test_tutorial/test_where/test_tutorial005.py @@ -1,21 +1,28 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial005 as mod - +@pytest.fixture( + name="mod", + params=[ + "tutorial005", + pytest.param("tutorial005_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.where.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [{"name": "Tarantula", "secret_name": "Natalia Roman-on", "age": 32, "id": 4}] ] diff --git a/tests/test_tutorial/test_where/test_tutorial005_py310.py b/tests/test_tutorial/test_where/test_tutorial005_py310.py deleted file mode 100644 index d238fff4f8..0000000000 --- a/tests/test_tutorial/test_where/test_tutorial005_py310.py +++ /dev/null @@ -1,22 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial005_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [{"name": "Tarantula", "secret_name": "Natalia Roman-on", "age": 32, "id": 4}] - ] diff --git a/tests/test_tutorial/test_where/test_tutorial006.py b/tests/test_tutorial/test_where/test_tutorial006.py index e5406dfbb0..e5d586a39a 100644 --- a/tests/test_tutorial/test_where/test_tutorial006.py +++ b/tests/test_tutorial/test_where/test_tutorial006.py @@ -1,22 +1,29 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial006 as mod - +@pytest.fixture( + name="mod", + params=[ + "tutorial006", + pytest.param("tutorial006_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.where.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [{"name": "Tarantula", "secret_name": "Natalia Roman-on", "age": 32, "id": 4}], [{"name": "Black Lion", "secret_name": "Trevor Challa", "age": 35, "id": 5}], ] diff --git a/tests/test_tutorial/test_where/test_tutorial006_py310.py b/tests/test_tutorial/test_where/test_tutorial006_py310.py deleted file mode 100644 index 8a4924fc09..0000000000 --- a/tests/test_tutorial/test_where/test_tutorial006_py310.py +++ /dev/null @@ -1,23 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial006_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [{"name": "Tarantula", "secret_name": "Natalia Roman-on", "age": 32, "id": 4}], - [{"name": "Black Lion", "secret_name": "Trevor Challa", "age": 35, "id": 5}], - ] diff --git a/tests/test_tutorial/test_where/test_tutorial007.py b/tests/test_tutorial/test_where/test_tutorial007.py index 878e81f932..9a36279d38 100644 --- a/tests/test_tutorial/test_where/test_tutorial007.py +++ b/tests/test_tutorial/test_where/test_tutorial007.py @@ -1,22 +1,29 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial007 as mod - +@pytest.fixture( + name="mod", + params=[ + "tutorial007", + pytest.param("tutorial007_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.where.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [{"id": 5, "name": "Black Lion", "secret_name": "Trevor Challa", "age": 35}], [{"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}], ] diff --git a/tests/test_tutorial/test_where/test_tutorial007_py310.py b/tests/test_tutorial/test_where/test_tutorial007_py310.py deleted file mode 100644 index a2110a19dc..0000000000 --- a/tests/test_tutorial/test_where/test_tutorial007_py310.py +++ /dev/null @@ -1,23 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial007_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [{"id": 5, "name": "Black Lion", "secret_name": "Trevor Challa", "age": 35}], - [{"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}], - ] diff --git a/tests/test_tutorial/test_where/test_tutorial008.py b/tests/test_tutorial/test_where/test_tutorial008.py index 08f4c49b9d..a21e2ecbe3 100644 --- a/tests/test_tutorial/test_where/test_tutorial008.py +++ b/tests/test_tutorial/test_where/test_tutorial008.py @@ -1,22 +1,29 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial008 as mod - +@pytest.fixture( + name="mod", + params=[ + "tutorial008", + pytest.param("tutorial008_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.where.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [{"id": 5, "name": "Black Lion", "secret_name": "Trevor Challa", "age": 35}], [{"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}], ] diff --git a/tests/test_tutorial/test_where/test_tutorial008_py310.py b/tests/test_tutorial/test_where/test_tutorial008_py310.py deleted file mode 100644 index 887ac70abd..0000000000 --- a/tests/test_tutorial/test_where/test_tutorial008_py310.py +++ /dev/null @@ -1,23 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial008_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [{"id": 5, "name": "Black Lion", "secret_name": "Trevor Challa", "age": 35}], - [{"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}], - ] diff --git a/tests/test_tutorial/test_where/test_tutorial009.py b/tests/test_tutorial/test_where/test_tutorial009.py index 2583f330cb..8088045b02 100644 --- a/tests/test_tutorial/test_where/test_tutorial009.py +++ b/tests/test_tutorial/test_where/test_tutorial009.py @@ -1,22 +1,29 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial009 as mod - +@pytest.fixture( + name="mod", + params=[ + "tutorial009", + pytest.param("tutorial009_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.where.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [{"name": "Tarantula", "secret_name": "Natalia Roman-on", "age": 32, "id": 4}], [{"name": "Black Lion", "secret_name": "Trevor Challa", "age": 35, "id": 5}], [ diff --git a/tests/test_tutorial/test_where/test_tutorial009_py310.py b/tests/test_tutorial/test_where/test_tutorial009_py310.py deleted file mode 100644 index 9bbef9b9f8..0000000000 --- a/tests/test_tutorial/test_where/test_tutorial009_py310.py +++ /dev/null @@ -1,31 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial009_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [{"name": "Tarantula", "secret_name": "Natalia Roman-on", "age": 32, "id": 4}], - [{"name": "Black Lion", "secret_name": "Trevor Challa", "age": 35, "id": 5}], - [ - { - "name": "Captain North America", - "secret_name": "Esteban Rogelios", - "age": 93, - "id": 7, - } - ], - ] diff --git a/tests/test_tutorial/test_where/test_tutorial010.py b/tests/test_tutorial/test_where/test_tutorial010.py index 71ef75d3a4..ea235ef708 100644 --- a/tests/test_tutorial/test_where/test_tutorial010.py +++ b/tests/test_tutorial/test_where/test_tutorial010.py @@ -1,22 +1,29 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial010 as mod - +@pytest.fixture( + name="mod", + params=[ + "tutorial010", + pytest.param("tutorial010_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.where.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() + assert print_mock.calls == [ [{"name": "Tarantula", "secret_name": "Natalia Roman-on", "age": 32, "id": 4}], [{"name": "Black Lion", "secret_name": "Trevor Challa", "age": 35, "id": 5}], [ diff --git a/tests/test_tutorial/test_where/test_tutorial010_py310.py b/tests/test_tutorial/test_where/test_tutorial010_py310.py deleted file mode 100644 index e990abed44..0000000000 --- a/tests/test_tutorial/test_where/test_tutorial010_py310.py +++ /dev/null @@ -1,31 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial010_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - assert calls == [ - [{"name": "Tarantula", "secret_name": "Natalia Roman-on", "age": 32, "id": 4}], - [{"name": "Black Lion", "secret_name": "Trevor Challa", "age": 35, "id": 5}], - [ - { - "name": "Captain North America", - "secret_name": "Esteban Rogelios", - "age": 93, - "id": 7, - } - ], - ] diff --git a/tests/test_tutorial/test_where/test_tutorial011.py b/tests/test_tutorial/test_where/test_tutorial011.py index 8006cd0708..ba53550611 100644 --- a/tests/test_tutorial/test_where/test_tutorial011.py +++ b/tests/test_tutorial/test_where/test_tutorial011.py @@ -1,21 +1,28 @@ -from unittest.mock import patch +import importlib +from types import ModuleType +import pytest from sqlmodel import create_engine -from ...conftest import get_testing_print_function +from ...conftest import PrintMock, needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial011 as mod - +@pytest.fixture( + name="mod", + params=[ + "tutorial011", + pytest.param("tutorial011_py310", marks=needs_py310), + ], +) +def get_module(request: pytest.FixtureRequest) -> ModuleType: + mod = importlib.import_module(f"docs_src.tutorial.where.{request.param}") mod.sqlite_url = "sqlite://" mod.engine = create_engine(mod.sqlite_url) - calls = [] + return mod - new_print = get_testing_print_function(calls) - with patch("builtins.print", new=new_print): - mod.main() +def test_tutorial(print_mock: PrintMock, mod: ModuleType): + mod.main() expected_calls = [ [{"id": 5, "name": "Black Lion", "secret_name": "Trevor Challa", "age": 35}], [{"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}], @@ -29,6 +36,7 @@ def test_tutorial(clear_sqlmodel): } ], ] + calls = print_mock.calls for call in expected_calls: assert call in calls, "This expected item should be in the list" # Now that this item was checked, remove it from the list diff --git a/tests/test_tutorial/test_where/test_tutorial011_py310.py b/tests/test_tutorial/test_where/test_tutorial011_py310.py deleted file mode 100644 index aee809b15b..0000000000 --- a/tests/test_tutorial/test_where/test_tutorial011_py310.py +++ /dev/null @@ -1,37 +0,0 @@ -from unittest.mock import patch - -from sqlmodel import create_engine - -from ...conftest import get_testing_print_function, needs_py310 - - -@needs_py310 -def test_tutorial(clear_sqlmodel): - from docs_src.tutorial.where import tutorial011_py310 as mod - - mod.sqlite_url = "sqlite://" - mod.engine = create_engine(mod.sqlite_url) - calls = [] - - new_print = get_testing_print_function(calls) - - with patch("builtins.print", new=new_print): - mod.main() - expected_calls = [ - [{"id": 5, "name": "Black Lion", "secret_name": "Trevor Challa", "age": 35}], - [{"id": 6, "name": "Dr. Weird", "secret_name": "Steve Weird", "age": 36}], - [{"id": 3, "name": "Rusty-Man", "secret_name": "Tommy Sharp", "age": 48}], - [ - { - "id": 7, - "name": "Captain North America", - "secret_name": "Esteban Rogelios", - "age": 93, - } - ], - ] - for call in expected_calls: - assert call in calls, "This expected item should be in the list" - # Now that this item was checked, remove it from the list - calls.pop(calls.index(call)) - assert len(calls) == 0, "The list should only have the expected items" From 3b5b92ec2bd509cde2fb4d8c175a8692e4658315 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 24 Dec 2025 15:47:30 +0000 Subject: [PATCH 878/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index cc30b327be..ae8b888ed4 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Internal +* ✅ Simplify tests for code examples, one test file for multiple variants. PR [#1664](https://github.com/fastapi/sqlmodel/pull/1664) by [@YuriiMotov](https://github.com/YuriiMotov). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1677](https://github.com/fastapi/sqlmodel/pull/1677) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump actions/download-artifact from 6 to 7. PR [#1676](https://github.com/fastapi/sqlmodel/pull/1676) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/cache from 4 to 5. PR [#1673](https://github.com/fastapi/sqlmodel/pull/1673) by [@dependabot[bot]](https://github.com/apps/dependabot). From ef7265c7f3f7d7d83bf1899189ef18cb7d00d00b Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Wed, 24 Dec 2025 16:49:07 +0100 Subject: [PATCH 879/906] =?UTF-8?q?=F0=9F=91=B7=20Add=20pre-commit=20workf?= =?UTF-8?q?low=20(#1684)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] --- .github/workflows/pre-commit.yml | 93 ++++++++++++++++++++++++++++++++ .github/workflows/test.yml | 3 -- .pre-commit-config.yaml | 5 -- requirements.txt | 2 +- 4 files changed, 94 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/pre-commit.yml diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000000..b397912e67 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,93 @@ +name: pre-commit + +on: + pull_request: + types: + - opened + - synchronize + +env: + # Forks and Dependabot don't have access to secrets + HAS_SECRETS: ${{ secrets.PRE_COMMIT != '' }} + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@v5 + name: Checkout PR for own repo + if: env.HAS_SECRETS == 'true' + with: + # To be able to commit it needs to fetch the head of the branch, not the + # merge commit + ref: ${{ github.head_ref }} + # And it needs the full history to be able to compute diffs + fetch-depth: 0 + # A token other than the default GITHUB_TOKEN is needed to be able to trigger CI + token: ${{ secrets.PRE_COMMIT }} + # pre-commit lite ci needs the default checkout configs to work + - uses: actions/checkout@v5 + name: Checkout PR for fork + if: env.HAS_SECRETS == 'false' + with: + # To be able to commit it needs the head branch of the PR, the remote one + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.14" + - name: Setup uv + uses: astral-sh/setup-uv@v7 + with: + cache-dependency-glob: | + requirements**.txt + pyproject.toml + uv.lock + - name: Install Dependencies + run: | + uv venv + uv pip install -r requirements.txt + - name: Run prek - pre-commit + id: precommit + run: uvx prek run --from-ref origin/${GITHUB_BASE_REF} --to-ref HEAD --show-diff-on-failure + continue-on-error: true + - name: Commit and push changes + if: env.HAS_SECRETS == 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add -A + if git diff --staged --quiet; then + echo "No changes to commit" + else + git commit -m "🎨 Auto format" + git push + fi + - uses: pre-commit-ci/lite-action@v1.1.0 + if: env.HAS_SECRETS == 'false' + with: + msg: 🎨 Auto format + - name: Error out on pre-commit errors + if: steps.precommit.outcome == 'failure' + run: exit 1 + + # https://github.com/marketplace/actions/alls-green#why + pre-commit-alls-green: # This job does nothing and is only used for the branch protection + if: always() + needs: + - pre-commit + runs-on: ubuntu-latest + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9738ecf570..72dff0d732 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -81,9 +81,6 @@ jobs: - name: Install Pydantic v2 if: matrix.pydantic-version == 'pydantic-v2' run: uv pip install --upgrade "pydantic>=2.0.2,<3.0.0" - - name: Lint - if: matrix.pydantic-version == 'pydantic-v2' && matrix.python-version != '3.8' - run: bash scripts/lint.sh - run: mkdir coverage - name: Test run: bash scripts/test.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 916fb47935..20a49e7a94 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,5 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks -default_language_version: - python: python3.10 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 @@ -20,6 +18,3 @@ repos: args: - --fix - id: ruff-format -ci: - autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks - autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate diff --git a/requirements.txt b/requirements.txt index f17705f380..01da92143d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,4 @@ -r requirements-tests.txt -r requirements-docs.txt -pre-commit >=2.17.0,<5.0.0 +prek==0.2.24 From 3c9ce4d0082afc9a590dc727aa72652198457a11 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 24 Dec 2025 15:49:26 +0000 Subject: [PATCH 880/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index ae8b888ed4..fc47ccf25d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Internal +* 👷 Add pre-commit workflow. PR [#1684](https://github.com/fastapi/sqlmodel/pull/1684) by [@YuriiMotov](https://github.com/YuriiMotov). * ✅ Simplify tests for code examples, one test file for multiple variants. PR [#1664](https://github.com/fastapi/sqlmodel/pull/1664) by [@YuriiMotov](https://github.com/YuriiMotov). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1677](https://github.com/fastapi/sqlmodel/pull/1677) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump actions/download-artifact from 6 to 7. PR [#1676](https://github.com/fastapi/sqlmodel/pull/1676) by [@dependabot[bot]](https://github.com/apps/dependabot). From 4097c17c8922d26622b5be05a45638b28501e3d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Dec 2025 23:10:18 +0100 Subject: [PATCH 881/906] =?UTF-8?q?=E2=AC=86=20Bump=20actions/checkout=20f?= =?UTF-8?q?rom=205=20to=206=20(#1692)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pre-commit.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index b397912e67..ff5340aeb7 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -18,7 +18,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 name: Checkout PR for own repo if: env.HAS_SECRETS == 'true' with: @@ -30,7 +30,7 @@ jobs: # A token other than the default GITHUB_TOKEN is needed to be able to trigger CI token: ${{ secrets.PRE_COMMIT }} # pre-commit lite ci needs the default checkout configs to work - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 name: Checkout PR for fork if: env.HAS_SECRETS == 'false' with: From d7b596b1dd7551b1c94f88390466ee4656ebdc01 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 24 Dec 2025 22:10:37 +0000 Subject: [PATCH 882/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index fc47ccf25d..9ddf7ed7c0 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Internal +* ⬆ Bump actions/checkout from 5 to 6. PR [#1692](https://github.com/fastapi/sqlmodel/pull/1692) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Add pre-commit workflow. PR [#1684](https://github.com/fastapi/sqlmodel/pull/1684) by [@YuriiMotov](https://github.com/YuriiMotov). * ✅ Simplify tests for code examples, one test file for multiple variants. PR [#1664](https://github.com/fastapi/sqlmodel/pull/1664) by [@YuriiMotov](https://github.com/YuriiMotov). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1677](https://github.com/fastapi/sqlmodel/pull/1677) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From afc0c324cf436e40043353b8d4e1150b922ab383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 26 Dec 2025 03:03:06 -0800 Subject: [PATCH 883/906] =?UTF-8?q?=E2=9E=96=20Drop=20support=20for=20Pyth?= =?UTF-8?q?on=203.8=20in=20CI=20and=20docs=20(#1695)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yurii Motov Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> --- .github/workflows/test.yml | 3 - docs/management-tasks.md | 2 +- docs/tutorial/automatic-id-none-refresh.md | 4 +- docs/tutorial/code-structure.md | 24 +-- docs/tutorial/create-db-and-table.md | 4 +- docs/tutorial/delete.md | 4 +- docs/tutorial/fastapi/tests.md | 38 ++-- docs/tutorial/insert.md | 10 +- docs/tutorial/select.md | 4 +- docs/tutorial/update.md | 12 +- .../{tutorial001.py => tutorial001_py39.py} | 0 .../{tutorial001.py => tutorial001_py39.py} | 0 .../{tutorial002.py => tutorial002_py39.py} | 0 .../{tutorial001.py => tutorial001_py39.py} | 0 .../{tutorial002.py => tutorial002_py39.py} | 0 .../code_structure/tutorial001/__init__.py | 0 .../code_structure/tutorial001/app.py | 29 --- .../code_structure/tutorial001/database.py | 10 - .../code_structure/tutorial001/models.py | 21 -- .../code_structure/tutorial002/__init__.py | 0 .../code_structure/tutorial002/app.py | 30 --- .../code_structure/tutorial002/database.py | 10 - .../code_structure/tutorial002/hero_model.py | 16 -- .../code_structure/tutorial002/team_model.py | 14 -- .../{tutorial001.py => tutorial001_py39.py} | 0 .../{tutorial001.py => tutorial001_py39.py} | 0 .../{tutorial001.py => tutorial001_py39.py} | 0 .../{tutorial001.py => tutorial001_py39.py} | 0 .../{tutorial002.py => tutorial002_py39.py} | 0 .../{tutorial003.py => tutorial003_py39.py} | 0 .../{tutorial004.py => tutorial004_py39.py} | 0 .../{tutorial005.py => tutorial005_py39.py} | 0 .../{tutorial001.py => tutorial001_py39.py} | 0 .../annotations/en/tutorial003.md | 4 +- .../{tutorial001.py => tutorial001_py39.py} | 0 .../{tutorial002.py => tutorial002_py39.py} | 0 .../{tutorial003.py => tutorial003_py39.py} | 0 .../{tutorial001.py => tutorial001_py39.py} | 0 .../{tutorial002.py => tutorial002_py39.py} | 0 .../app_testing/tutorial001/__init__.py | 0 .../annotations/en/test_main_001.md | 17 -- .../annotations/en/test_main_002.md | 25 --- .../annotations/en/test_main_003.md | 37 ---- .../annotations/en/test_main_004.md | 29 --- .../annotations/en/test_main_005.md | 41 ---- .../annotations/en/test_main_006.md | 23 -- .../fastapi/app_testing/tutorial001/main.py | 105 --------- .../tutorial001/test_extra_coverage.py | 38 ---- .../app_testing/tutorial001/test_main.py | 125 ----------- .../app_testing/tutorial001/test_main_001.py | 32 --- .../app_testing/tutorial001/test_main_002.py | 32 --- .../app_testing/tutorial001/test_main_003.py | 33 --- .../app_testing/tutorial001/test_main_004.py | 35 --- .../app_testing/tutorial001/test_main_005.py | 37 ---- .../app_testing/tutorial001/test_main_006.py | 41 ---- .../tutorial/fastapi/delete/tutorial001.py | 98 --------- .../fastapi/limit_and_offset/tutorial001.py | 67 ------ .../fastapi/multiple_models/tutorial001.py | 60 ------ .../fastapi/multiple_models/tutorial002.py | 58 ----- .../tutorial/fastapi/read_one/tutorial001.py | 67 ------ .../fastapi/relationships/tutorial001.py | 199 ------------------ .../fastapi/response_model/tutorial001.py | 46 ---- .../session_with_dependency/tutorial001.py | 105 --------- .../{tutorial001.py => tutorial001_py39.py} | 0 .../tutorial/fastapi/teams/tutorial001.py | 190 ----------------- .../tutorial/fastapi/update/tutorial001.py | 87 -------- .../tutorial/fastapi/update/tutorial002.py | 101 --------- .../{tutorial001.py => tutorial001_py39.py} | 0 .../{tutorial002.py => tutorial002_py39.py} | 0 .../{tutorial001.py => tutorial001_py39.py} | 0 .../{tutorial002.py => tutorial002_py39.py} | 0 .../{tutorial003.py => tutorial003_py39.py} | 0 docs_src/tutorial/many_to_many/tutorial001.py | 84 -------- docs_src/tutorial/many_to_many/tutorial002.py | 107 ---------- docs_src/tutorial/many_to_many/tutorial003.py | 123 ----------- .../{tutorial001.py => tutorial001_py39.py} | 0 .../{tutorial002.py => tutorial002_py39.py} | 0 .../{tutorial003.py => tutorial003_py39.py} | 0 .../{tutorial004.py => tutorial004_py39.py} | 0 .../{tutorial001.py => tutorial001_py39.py} | 0 .../{tutorial002.py => tutorial002_py39.py} | 0 .../{tutorial003.py => tutorial003_py39.py} | 0 .../{tutorial004.py => tutorial004_py39.py} | 0 .../{tutorial005.py => tutorial005_py39.py} | 0 .../{tutorial006.py => tutorial006_py39.py} | 0 .../{tutorial007.py => tutorial007_py39.py} | 0 .../{tutorial008.py => tutorial008_py39.py} | 0 .../{tutorial009.py => tutorial009_py39.py} | 0 .../back_populates/tutorial001.py | 143 ------------- .../back_populates/tutorial002.py | 143 ------------- .../back_populates/tutorial003.py | 59 ------ .../tutorial001.py | 110 ---------- .../tutorial002.py | 110 ---------- .../tutorial003.py | 112 ---------- .../tutorial004.py | 111 ---------- .../tutorial005.py | 124 ----------- .../tutorial001.py | 102 --------- .../tutorial001.py | 70 ------ .../read_relationships/tutorial001.py | 117 ---------- .../read_relationships/tutorial002.py | 127 ----------- .../{tutorial001.py => tutorial001_py39.py} | 0 .../{tutorial002.py => tutorial002_py39.py} | 0 .../{tutorial003.py => tutorial003_py39.py} | 0 .../{tutorial004.py => tutorial004_py39.py} | 0 .../{tutorial001.py => tutorial001_py39.py} | 0 .../{tutorial002.py => tutorial002_py39.py} | 0 .../{tutorial003.py => tutorial003_py39.py} | 0 .../{tutorial004.py => tutorial004_py39.py} | 0 .../{tutorial001.py => tutorial001_py39.py} | 0 .../{tutorial002.py => tutorial002_py39.py} | 0 .../{tutorial003.py => tutorial003_py39.py} | 0 .../{tutorial004.py => tutorial004_py39.py} | 0 .../{tutorial005.py => tutorial005_py39.py} | 0 .../{tutorial006.py => tutorial006_py39.py} | 0 .../{tutorial007.py => tutorial007_py39.py} | 0 .../{tutorial008.py => tutorial008_py39.py} | 0 .../{tutorial009.py => tutorial009_py39.py} | 0 .../{tutorial010.py => tutorial010_py39.py} | 0 .../{tutorial011.py => tutorial011_py39.py} | 0 requirements-tests.txt | 8 +- tests/conftest.py | 3 +- .../test_decimal/test_tutorial001.py | 2 +- .../test_uuid/test_tutorial001.py | 2 +- .../test_uuid/test_tutorial002.py | 2 +- tests/test_select_gen.py | 3 - .../test_tutorial001_tutorial002.py | 4 +- .../test_code_structure/test_tutorial001.py | 5 +- .../test_code_structure/test_tutorial002.py | 5 +- .../test_tutorial001.py | 2 +- .../test_delete/test_tutorial001.py | 2 +- .../test_insert/test_tutorial001.py | 2 +- .../test_tutorial001_tutorial002.py | 4 +- .../test_select/test_tutorial003.py | 2 +- .../test_select/test_tutorial004.py | 2 +- .../test_select/test_tutorial005.py | 2 +- .../test_update/test_tutorial001.py | 2 +- .../test_tutorial001.py | 2 +- .../test_tutorial002.py | 2 +- .../test_tutorial003.py | 2 +- .../test_tutorial001_tutorial002.py | 4 +- .../test_tutorial001_tests001.py | 5 +- .../test_tutorial001_tests002.py | 7 +- .../test_tutorial001_tests003.py | 7 +- .../test_tutorial001_tests004.py | 7 +- .../test_tutorial001_tests005.py | 8 +- .../test_tutorial001_tests006.py | 8 +- .../test_tutorial001_tests_main.py | 5 +- .../test_delete/test_tutorial001.py | 5 +- .../test_limit_and_offset/test_tutorial001.py | 5 +- .../test_multiple_models/test_tutorial001.py | 5 +- .../test_multiple_models/test_tutorial002.py | 5 +- .../test_read_one/test_tutorial001.py | 5 +- .../test_relationships/test_tutorial001.py | 5 +- .../test_response_model/test_tutorial001.py | 5 +- .../test_tutorial001.py | 5 +- .../test_simple_hero_api/test_tutorial001.py | 2 +- .../test_teams/test_tutorial001.py | 5 +- .../test_update/test_tutorial001.py | 5 +- .../test_update/test_tutorial002.py | 5 +- .../test_indexes/test_tutorial001.py | 2 +- .../test_indexes/test_tutorial002.py | 2 +- .../test_insert/test_tutorial001.py | 2 +- .../test_insert/test_tutorial002.py | 2 +- .../test_insert/test_tutorial003.py | 2 +- .../test_limit_and_offset/test_tutorial001.py | 2 +- .../test_limit_and_offset/test_tutorial002.py | 2 +- .../test_limit_and_offset/test_tutorial003.py | 2 +- .../test_limit_and_offset/test_tutorial004.py | 2 +- .../test_many_to_many/test_tutorial001.py | 5 +- .../test_many_to_many/test_tutorial002.py | 5 +- .../test_many_to_many/test_tutorial003.py | 5 +- .../test_one/test_tutorial001.py | 2 +- .../test_one/test_tutorial002.py | 2 +- .../test_one/test_tutorial003.py | 2 +- .../test_one/test_tutorial004.py | 2 +- .../test_one/test_tutorial005.py | 2 +- .../test_one/test_tutorial006.py | 2 +- .../test_one/test_tutorial007.py | 2 +- .../test_one/test_tutorial008.py | 2 +- .../test_one/test_tutorial009.py | 2 +- .../test_back_populates/test_tutorial001.py | 5 +- .../test_back_populates/test_tutorial002.py | 5 +- .../test_back_populates/test_tutorial003.py | 5 +- .../test_tutorial001.py | 5 +- .../test_tutorial001.py | 5 +- .../test_tutorial001.py | 5 +- .../test_tutorial002.py | 5 +- .../test_tutorial003.py | 5 +- .../test_tutorial004.py | 5 +- .../test_tutorial005.py | 5 +- .../test_tutorial001.py | 5 +- .../test_tutorial002.py | 5 +- .../test_tutorial001_tutorial002.py | 4 +- .../test_tutorial003_tutorial004.py | 4 +- .../test_tutorial001_tutorial002.py | 4 +- .../test_tutorial003_tutorial004.py | 4 +- .../test_where/test_tutorial001.py | 2 +- .../test_where/test_tutorial002.py | 2 +- .../test_where/test_tutorial003.py | 2 +- .../test_where/test_tutorial004.py | 2 +- .../test_where/test_tutorial005.py | 2 +- .../test_where/test_tutorial006.py | 2 +- .../test_where/test_tutorial007.py | 2 +- .../test_where/test_tutorial008.py | 2 +- .../test_where/test_tutorial009.py | 2 +- .../test_where/test_tutorial010.py | 2 +- .../test_where/test_tutorial011.py | 2 +- 207 files changed, 186 insertions(+), 3738 deletions(-) rename docs_src/advanced/decimal/{tutorial001.py => tutorial001_py39.py} (100%) rename docs_src/advanced/uuid/{tutorial001.py => tutorial001_py39.py} (100%) rename docs_src/advanced/uuid/{tutorial002.py => tutorial002_py39.py} (100%) rename docs_src/tutorial/automatic_id_none_refresh/{tutorial001.py => tutorial001_py39.py} (100%) rename docs_src/tutorial/automatic_id_none_refresh/{tutorial002.py => tutorial002_py39.py} (100%) delete mode 100644 docs_src/tutorial/code_structure/tutorial001/__init__.py delete mode 100644 docs_src/tutorial/code_structure/tutorial001/app.py delete mode 100644 docs_src/tutorial/code_structure/tutorial001/database.py delete mode 100644 docs_src/tutorial/code_structure/tutorial001/models.py delete mode 100644 docs_src/tutorial/code_structure/tutorial002/__init__.py delete mode 100644 docs_src/tutorial/code_structure/tutorial002/app.py delete mode 100644 docs_src/tutorial/code_structure/tutorial002/database.py delete mode 100644 docs_src/tutorial/code_structure/tutorial002/hero_model.py delete mode 100644 docs_src/tutorial/code_structure/tutorial002/team_model.py rename docs_src/tutorial/connect/create_tables/{tutorial001.py => tutorial001_py39.py} (100%) rename docs_src/tutorial/connect/delete/{tutorial001.py => tutorial001_py39.py} (100%) rename docs_src/tutorial/connect/insert/{tutorial001.py => tutorial001_py39.py} (100%) rename docs_src/tutorial/connect/select/{tutorial001.py => tutorial001_py39.py} (100%) rename docs_src/tutorial/connect/select/{tutorial002.py => tutorial002_py39.py} (100%) rename docs_src/tutorial/connect/select/{tutorial003.py => tutorial003_py39.py} (100%) rename docs_src/tutorial/connect/select/{tutorial004.py => tutorial004_py39.py} (100%) rename docs_src/tutorial/connect/select/{tutorial005.py => tutorial005_py39.py} (100%) rename docs_src/tutorial/connect/update/{tutorial001.py => tutorial001_py39.py} (100%) rename docs_src/tutorial/create_db_and_table/{tutorial001.py => tutorial001_py39.py} (100%) rename docs_src/tutorial/create_db_and_table/{tutorial002.py => tutorial002_py39.py} (100%) rename docs_src/tutorial/create_db_and_table/{tutorial003.py => tutorial003_py39.py} (100%) rename docs_src/tutorial/delete/{tutorial001.py => tutorial001_py39.py} (100%) rename docs_src/tutorial/delete/{tutorial002.py => tutorial002_py39.py} (100%) delete mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001/__init__.py delete mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_001.md delete mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_002.md delete mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_003.md delete mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_004.md delete mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_005.md delete mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_006.md delete mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001/main.py delete mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001/test_extra_coverage.py delete mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001/test_main.py delete mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_001.py delete mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_002.py delete mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_003.py delete mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_004.py delete mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_005.py delete mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_006.py delete mode 100644 docs_src/tutorial/fastapi/delete/tutorial001.py delete mode 100644 docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py delete mode 100644 docs_src/tutorial/fastapi/multiple_models/tutorial001.py delete mode 100644 docs_src/tutorial/fastapi/multiple_models/tutorial002.py delete mode 100644 docs_src/tutorial/fastapi/read_one/tutorial001.py delete mode 100644 docs_src/tutorial/fastapi/relationships/tutorial001.py delete mode 100644 docs_src/tutorial/fastapi/response_model/tutorial001.py delete mode 100644 docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py rename docs_src/tutorial/fastapi/simple_hero_api/{tutorial001.py => tutorial001_py39.py} (100%) delete mode 100644 docs_src/tutorial/fastapi/teams/tutorial001.py delete mode 100644 docs_src/tutorial/fastapi/update/tutorial001.py delete mode 100644 docs_src/tutorial/fastapi/update/tutorial002.py rename docs_src/tutorial/indexes/{tutorial001.py => tutorial001_py39.py} (100%) rename docs_src/tutorial/indexes/{tutorial002.py => tutorial002_py39.py} (100%) rename docs_src/tutorial/insert/{tutorial001.py => tutorial001_py39.py} (100%) rename docs_src/tutorial/insert/{tutorial002.py => tutorial002_py39.py} (100%) rename docs_src/tutorial/insert/{tutorial003.py => tutorial003_py39.py} (100%) delete mode 100644 docs_src/tutorial/many_to_many/tutorial001.py delete mode 100644 docs_src/tutorial/many_to_many/tutorial002.py delete mode 100644 docs_src/tutorial/many_to_many/tutorial003.py rename docs_src/tutorial/offset_and_limit/{tutorial001.py => tutorial001_py39.py} (100%) rename docs_src/tutorial/offset_and_limit/{tutorial002.py => tutorial002_py39.py} (100%) rename docs_src/tutorial/offset_and_limit/{tutorial003.py => tutorial003_py39.py} (100%) rename docs_src/tutorial/offset_and_limit/{tutorial004.py => tutorial004_py39.py} (100%) rename docs_src/tutorial/one/{tutorial001.py => tutorial001_py39.py} (100%) rename docs_src/tutorial/one/{tutorial002.py => tutorial002_py39.py} (100%) rename docs_src/tutorial/one/{tutorial003.py => tutorial003_py39.py} (100%) rename docs_src/tutorial/one/{tutorial004.py => tutorial004_py39.py} (100%) rename docs_src/tutorial/one/{tutorial005.py => tutorial005_py39.py} (100%) rename docs_src/tutorial/one/{tutorial006.py => tutorial006_py39.py} (100%) rename docs_src/tutorial/one/{tutorial007.py => tutorial007_py39.py} (100%) rename docs_src/tutorial/one/{tutorial008.py => tutorial008_py39.py} (100%) rename docs_src/tutorial/one/{tutorial009.py => tutorial009_py39.py} (100%) delete mode 100644 docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py delete mode 100644 docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py delete mode 100644 docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py delete mode 100644 docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001.py delete mode 100644 docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002.py delete mode 100644 docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003.py delete mode 100644 docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004.py delete mode 100644 docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005.py delete mode 100644 docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py delete mode 100644 docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py delete mode 100644 docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py delete mode 100644 docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py rename docs_src/tutorial/select/{tutorial001.py => tutorial001_py39.py} (100%) rename docs_src/tutorial/select/{tutorial002.py => tutorial002_py39.py} (100%) rename docs_src/tutorial/select/{tutorial003.py => tutorial003_py39.py} (100%) rename docs_src/tutorial/select/{tutorial004.py => tutorial004_py39.py} (100%) rename docs_src/tutorial/update/{tutorial001.py => tutorial001_py39.py} (100%) rename docs_src/tutorial/update/{tutorial002.py => tutorial002_py39.py} (100%) rename docs_src/tutorial/update/{tutorial003.py => tutorial003_py39.py} (100%) rename docs_src/tutorial/update/{tutorial004.py => tutorial004_py39.py} (100%) rename docs_src/tutorial/where/{tutorial001.py => tutorial001_py39.py} (100%) rename docs_src/tutorial/where/{tutorial002.py => tutorial002_py39.py} (100%) rename docs_src/tutorial/where/{tutorial003.py => tutorial003_py39.py} (100%) rename docs_src/tutorial/where/{tutorial004.py => tutorial004_py39.py} (100%) rename docs_src/tutorial/where/{tutorial005.py => tutorial005_py39.py} (100%) rename docs_src/tutorial/where/{tutorial006.py => tutorial006_py39.py} (100%) rename docs_src/tutorial/where/{tutorial007.py => tutorial007_py39.py} (100%) rename docs_src/tutorial/where/{tutorial008.py => tutorial008_py39.py} (100%) rename docs_src/tutorial/where/{tutorial009.py => tutorial009_py39.py} (100%) rename docs_src/tutorial/where/{tutorial010.py => tutorial010_py39.py} (100%) rename docs_src/tutorial/where/{tutorial011.py => tutorial011_py39.py} (100%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 72dff0d732..2fea4779a9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,9 +30,6 @@ jobs: pydantic-version: - pydantic-v2 include: - - os: macos-latest - python-version: "3.8" - pydantic-version: pydantic-v1 - os: windows-latest python-version: "3.9" pydantic-version: pydantic-v2 diff --git a/docs/management-tasks.md b/docs/management-tasks.md index f8deb992f0..1ca3765c03 100644 --- a/docs/management-tasks.md +++ b/docs/management-tasks.md @@ -90,7 +90,7 @@ A PR should have a specific use case that it is solving. * If the PR is for a feature, it should have docs. * Unless it's a feature we want to discourage, like support for a corner case that we don't want users to use. * The docs should include a source example file, not write Python directly in Markdown. -* If the source example(s) file can have different syntax for Python 3.8, 3.9, 3.10, there should be different versions of the file, and they should be shown in tabs in the docs. +* If the source example(s) file can have different syntax for different Python versions, there should be different versions of the file, and they should be shown in tabs in the docs. * There should be tests testing the source example. * Before the PR is applied, the new tests should fail. * After applying the PR, the new tests should pass. diff --git a/docs/tutorial/automatic-id-none-refresh.md b/docs/tutorial/automatic-id-none-refresh.md index 0e67633dee..93842c6906 100644 --- a/docs/tutorial/automatic-id-none-refresh.md +++ b/docs/tutorial/automatic-id-none-refresh.md @@ -342,10 +342,10 @@ And as we created the **engine** with `echo=True`, we can see the SQL statements //// -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```Python -{!./docs_src/tutorial/automatic_id_none_refresh/tutorial002.py!} +{!./docs_src/tutorial/automatic_id_none_refresh/tutorial002_py39.py!} ``` {!./docs_src/tutorial/automatic_id_none_refresh/annotations/en/tutorial002.md!} diff --git a/docs/tutorial/code-structure.md b/docs/tutorial/code-structure.md index 6e377b89e4..08a4165429 100644 --- a/docs/tutorial/code-structure.md +++ b/docs/tutorial/code-structure.md @@ -67,9 +67,7 @@ We can use these relative imports because, for example, in the file `app.py` (th You could put all the database Models in a single Python module (a single Python file), for example `models.py`: -```Python -{!./docs_src/tutorial/code_structure/tutorial001/models.py!} -``` +{* ./docs_src/tutorial/code_structure/tutorial001_py310/models.py *} This way, you wouldn't have to deal with circular imports for other models. @@ -79,9 +77,7 @@ And then you could import the models from this file/module in any other file/mod Then you could put the code creating the **engine** and the function to create all the tables (if you are not using migrations) in another file `database.py`: -```Python -{!./docs_src/tutorial/code_structure/tutorial001/database.py!} -``` +{* ./docs_src/tutorial/code_structure/tutorial001_py310/database.py *} This file would also be imported by your application code, to use the shared **engine** and to get and call the function `create_db_and_tables()`. @@ -89,9 +85,7 @@ This file would also be imported by your application code, to use the shared **e Finally, you could put the code to create the **app** in another file `app.py`: -```Python hl_lines="3-4" -{!./docs_src/tutorial/code_structure/tutorial001/app.py!} -``` +{* ./docs_src/tutorial/code_structure/tutorial001_py310/app.py hl[3:4] *} Here we import the models, the engine, and the function to create all the tables and then we can use them all internally. @@ -207,9 +201,7 @@ So, we can use it in an `if` block and import things inside the `if` block. And Using that trick of `TYPE_CHECKING` we can "import" the `Team` in `hero_model.py`: -```Python hl_lines="1 5-6 16" -{!./docs_src/tutorial/code_structure/tutorial002/hero_model.py!} -``` +{* ./docs_src/tutorial/code_structure/tutorial002_py310/hero_model.py hl[1,5:6,16] *} Have in mind that now we *have* to put the annotation of `Team` as a string: `"Team"`, so that Python doesn't have errors at runtime. @@ -217,9 +209,7 @@ Have in mind that now we *have* to put the annotation of `Team` as a string: `"T We use the same trick in the `team_model.py` file: -```Python hl_lines="1 5-6 14" -{!./docs_src/tutorial/code_structure/tutorial002/team_model.py!} -``` +{* ./docs_src/tutorial/code_structure/tutorial002_py310/team_model.py hl[1,5:6,14] *} Now we get editor support, autocompletion, inline errors, and **SQLModel** keeps working. 🎉 @@ -227,9 +217,7 @@ Now we get editor support, autocompletion, inline errors, and **SQLModel** keeps Now, just for completeness, the `app.py` file would import the models from both modules: -```Python hl_lines="4-5 10 12-14" -{!./docs_src/tutorial/code_structure/tutorial002/app.py!} -``` +{* ./docs_src/tutorial/code_structure/tutorial002_py310/app.py hl[4:5,10,12:14] *} And of course, all the tricks with `TYPE_CHECKING` and type annotations in strings are **only needed in the files with circular imports**. diff --git a/docs/tutorial/create-db-and-table.md b/docs/tutorial/create-db-and-table.md index 688567ed4d..42ec604932 100644 --- a/docs/tutorial/create-db-and-table.md +++ b/docs/tutorial/create-db-and-table.md @@ -562,10 +562,10 @@ Now, let's give the code a final look: //// -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```{.python .annotate} -{!./docs_src/tutorial/create_db_and_table/tutorial003.py!} +{!./docs_src/tutorial/create_db_and_table/tutorial003_py39.py!} ``` {!./docs_src/tutorial/create_db_and_table/annotations/en/tutorial003.md!} diff --git a/docs/tutorial/delete.md b/docs/tutorial/delete.md index b0eaf6788b..9f494ec44c 100644 --- a/docs/tutorial/delete.md +++ b/docs/tutorial/delete.md @@ -227,10 +227,10 @@ Now let's review all that code: //// -//// tab | Python 3.8+ +//// tab | Python 3.9+ ```{ .python .annotate hl_lines="72-90" } -{!./docs_src/tutorial/delete/tutorial002.py!} +{!./docs_src/tutorial/delete/tutorial002_py39.py!} ``` {!./docs_src/tutorial/delete/annotations/en/tutorial002.md!} diff --git a/docs/tutorial/fastapi/tests.md b/docs/tutorial/fastapi/tests.md index ed4f91bcd6..f7fd92c9cb 100644 --- a/docs/tutorial/fastapi/tests.md +++ b/docs/tutorial/fastapi/tests.md @@ -14,7 +14,7 @@ We will use the application with the hero models, but without team models, and w Now we will see how useful it is to have this session dependency. ✨ -{* ./docs_src/tutorial/fastapi/app_testing/tutorial001/main.py ln[0] *} +{* ./docs_src/tutorial/fastapi/app_testing/tutorial001_py310/main.py ln[0] *} ## File Structure @@ -53,16 +53,16 @@ $ pip install requests pytest Let's start with a simple test, with just the basic test code we need the check that the **FastAPI** application is creating a new hero correctly. ```{ .python .annotate } -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_001.py[ln:1-7]!} +{!./docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_001.py[ln:1-7]!} # Some code here omitted, we will see it later 👈 -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_001.py[ln:20-24]!} +{!./docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_001.py[ln:20-24]!} # Some code here omitted, we will see it later 👈 -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_001.py[ln:26-32]!} +{!./docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_001.py[ln:26-32]!} # Code below omitted 👇 ``` -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_001.md!} +{!./docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_001.md!} /// tip @@ -103,14 +103,14 @@ We will override it to use a different **session** object just for the tests. That way we protect the production database and we have better control of the data we are testing. ```{ .python .annotate hl_lines="4 9-10 12 19" } -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_002.py[ln:1-7]!} +{!./docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_002.py[ln:1-7]!} # Some code here omitted, we will see it later 👈 -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_002.py[ln:15-32]!} +{!./docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_002.py[ln:15-32]!} # Code below omitted 👇 ``` -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_002.md!} +{!./docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_002.md!} /// tip @@ -131,10 +131,10 @@ sqlite:///testing.db So, the testing database will be in the file `testing.db`. ``` { .python .annotate hl_lines="4 8-11 13 16 33"} -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_003.py!} +{!./docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_003.py!} ``` -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_003.md!} +{!./docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_003.md!} ### Import Table Models @@ -187,12 +187,12 @@ Let's update our code to use the in-memory database. We just have to change a couple of parameters in the **engine**. ```{ .python .annotate hl_lines="3 9-13"} -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_004.py[ln:1-13]!} +{!./docs_src/tutorial/fastapi/app_testing/tutorial001_py310/test_main_004.py[ln:1-13]!} # Code below omitted 👇 ``` -{!./docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_004.md!} +{!./docs_src/tutorial/fastapi/app_testing/tutorial001_py310/annotations/en/test_main_004.md!} /// tip @@ -235,10 +235,10 @@ You can read more about them in the SQLAlchemy documentation about Using a Memory Database in Multiple Threads - - /// diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_005.md b/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_005.md deleted file mode 100644 index 126e1f1790..0000000000 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_005.md +++ /dev/null @@ -1,41 +0,0 @@ -1. Import `pytest`. - -2. Use the `@pytest.fixture()` decorator on top of the function to tell pytest that this is a **fixture** function (equivalent to a FastAPI dependency). - - We also give it a name of `"session"`, this will be important in the testing function. - -3. Create the fixture function. This is equivalent to a FastAPI dependency function. - - In this fixture we create the custom **engine**, with the in-memory database, we create the tables, and we create the **session**. - - Then we `yield` the `session` object. - -4. The thing that we `return` or `yield` is what will be available to the test function, in this case, the `session` object. - - Here we use `yield` so that **pytest** comes back to execute "the rest of the code" in this function once the testing function is done. - - We don't have any more visible "rest of the code" after the `yield`, but we have the end of the `with` block that will close the **session**. - - By using `yield`, pytest will: - - * run the first part - * create the **session** object - * give it to the test function - * run the test function - * once the test function is done, it will continue here, right after the `yield`, and will correctly close the **session** object in the end of the `with` block. - -5. Now, in the test function, to tell **pytest** that this test wants to get the fixture, instead of declaring something like in FastAPI with: - - ```Python - session: Session = Depends(session_fixture) - ``` - - ...the way we tell pytest what is the fixture that we want is by using the **exact same name** of the fixture. - - In this case, we named it `session`, so the parameter has to be exactly named `session` for it to work. - - We also add the type annotation `session: Session` so that we can get autocompletion and inline error checks in our editor. - -6. Now in the dependency override function, we just return the same `session` object that came from outside it. - - The `session` object comes from the parameter passed to the test function, and we just re-use it and return it here in the dependency override. diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_006.md b/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_006.md deleted file mode 100644 index d44a3b67da..0000000000 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_006.md +++ /dev/null @@ -1,23 +0,0 @@ -1. Create the new fixture named `"client"`. - -2. This **client fixture**, in turn, also requires the **session fixture**. - -3. Now we create the **dependency override** inside the client fixture. - -4. Set the **dependency override** in the `app.dependency_overrides` dictionary. - -5. Create the `TestClient` with the **FastAPI** `app`. - -6. `yield` the `TestClient` instance. - - By using `yield`, after the test function is done, pytest will come back to execute the rest of the code after `yield`. - -7. This is the cleanup code, after `yield`, and after the test function is done. - - Here we clear the dependency overrides (here it's only one) in the FastAPI `app`. - -8. Now the test function requires the **client fixture**. - - And inside the test function, the code is quite **simple**, we just use the `TestClient` to make requests to the API, check the data, and that's it. - - The fixtures take care of all the **setup** and **cleanup** code. diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py b/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py deleted file mode 100644 index f0a2559467..0000000000 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001/main.py +++ /dev/null @@ -1,105 +0,0 @@ -from typing import List, Optional - -from fastapi import Depends, FastAPI, HTTPException, Query -from sqlmodel import Field, Session, SQLModel, create_engine, select - - -class HeroBase(SQLModel): - name: str = Field(index=True) - secret_name: str - age: Optional[int] = Field(default=None, index=True) - - -class Hero(HeroBase, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - - -class HeroCreate(HeroBase): - pass - - -class HeroPublic(HeroBase): - id: int - - -class HeroUpdate(SQLModel): - name: Optional[str] = None - secret_name: Optional[str] = None - age: Optional[int] = None - - -sqlite_file_name = "database.db" -sqlite_url = f"sqlite:///{sqlite_file_name}" - -connect_args = {"check_same_thread": False} -engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) - - -def create_db_and_tables(): - SQLModel.metadata.create_all(engine) - - -def get_session(): - with Session(engine) as session: - yield session - - -app = FastAPI() - - -@app.on_event("startup") -def on_startup(): - create_db_and_tables() - - -@app.post("/heroes/", response_model=HeroPublic) -def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): - db_hero = Hero.model_validate(hero) - session.add(db_hero) - session.commit() - session.refresh(db_hero) - return db_hero - - -@app.get("/heroes/", response_model=List[HeroPublic]) -def read_heroes( - *, - session: Session = Depends(get_session), - offset: int = 0, - limit: int = Query(default=100, le=100), -): - heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() - return heroes - - -@app.get("/heroes/{hero_id}", response_model=HeroPublic) -def read_hero(*, session: Session = Depends(get_session), hero_id: int): - hero = session.get(Hero, hero_id) - if not hero: - raise HTTPException(status_code=404, detail="Hero not found") - return hero - - -@app.patch("/heroes/{hero_id}", response_model=HeroPublic) -def update_hero( - *, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate -): - db_hero = session.get(Hero, hero_id) - if not db_hero: - raise HTTPException(status_code=404, detail="Hero not found") - hero_data = hero.model_dump(exclude_unset=True) - db_hero.sqlmodel_update(hero_data) - session.add(db_hero) - session.commit() - session.refresh(db_hero) - return db_hero - - -@app.delete("/heroes/{hero_id}") -def delete_hero(*, session: Session = Depends(get_session), hero_id: int): - hero = session.get(Hero, hero_id) - if not hero: - raise HTTPException(status_code=404, detail="Hero not found") - session.delete(hero) - session.commit() - return {"ok": True} diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_extra_coverage.py b/docs_src/tutorial/fastapi/app_testing/tutorial001/test_extra_coverage.py deleted file mode 100644 index 1d8153ab9f..0000000000 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_extra_coverage.py +++ /dev/null @@ -1,38 +0,0 @@ -from fastapi.testclient import TestClient -from sqlalchemy import Inspector, inspect -from sqlmodel import Session, create_engine - -from . import main as app_mod -from .test_main import client_fixture, session_fixture - -assert client_fixture, "This keeps the client fixture used below" -assert session_fixture, "This keeps the session fixture used by client_fixture" - - -def test_startup(): - app_mod.engine = create_engine("sqlite://") - app_mod.on_startup() - insp: Inspector = inspect(app_mod.engine) - assert insp.has_table(str(app_mod.Hero.__tablename__)) - - -def test_get_session(): - app_mod.engine = create_engine("sqlite://") - for session in app_mod.get_session(): - assert isinstance(session, Session) - assert session.bind == app_mod.engine - - -def test_read_hero_not_found(client: TestClient): - response = client.get("/heroes/9000") - assert response.status_code == 404 - - -def test_update_hero_not_found(client: TestClient): - response = client.patch("/heroes/9000", json={"name": "Very-Rusty-Man"}) - assert response.status_code == 404 - - -def test_delete_hero_not_found(client: TestClient): - response = client.delete("/heroes/9000") - assert response.status_code == 404 diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main.py b/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main.py deleted file mode 100644 index 435787c79b..0000000000 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main.py +++ /dev/null @@ -1,125 +0,0 @@ -import pytest -from fastapi.testclient import TestClient -from sqlmodel import Session, SQLModel, create_engine -from sqlmodel.pool import StaticPool - -from .main import Hero, app, get_session - - -@pytest.fixture(name="session") -def session_fixture(): - engine = create_engine( - "sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool - ) - SQLModel.metadata.create_all(engine) - with Session(engine) as session: - yield session - - -@pytest.fixture(name="client") -def client_fixture(session: Session): - def get_session_override(): - return session - - app.dependency_overrides[get_session] = get_session_override - client = TestClient(app) - yield client - app.dependency_overrides.clear() - - -def test_create_hero(client: TestClient): - response = client.post( - "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} - ) - data = response.json() - - assert response.status_code == 200 - assert data["name"] == "Deadpond" - assert data["secret_name"] == "Dive Wilson" - assert data["age"] is None - assert data["id"] is not None - - -def test_create_hero_incomplete(client: TestClient): - # No secret_name - response = client.post("/heroes/", json={"name": "Deadpond"}) - assert response.status_code == 422 - - -def test_create_hero_invalid(client: TestClient): - # secret_name has an invalid type - response = client.post( - "/heroes/", - json={ - "name": "Deadpond", - "secret_name": {"message": "Do you wanna know my secret identity?"}, - }, - ) - assert response.status_code == 422 - - -def test_read_heroes(session: Session, client: TestClient): - hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") - hero_2 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) - session.add(hero_1) - session.add(hero_2) - session.commit() - - response = client.get("/heroes/") - data = response.json() - - assert response.status_code == 200 - - assert len(data) == 2 - assert data[0]["name"] == hero_1.name - assert data[0]["secret_name"] == hero_1.secret_name - assert data[0]["age"] == hero_1.age - assert data[0]["id"] == hero_1.id - assert data[1]["name"] == hero_2.name - assert data[1]["secret_name"] == hero_2.secret_name - assert data[1]["age"] == hero_2.age - assert data[1]["id"] == hero_2.id - - -def test_read_hero(session: Session, client: TestClient): - hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") - session.add(hero_1) - session.commit() - - response = client.get(f"/heroes/{hero_1.id}") - data = response.json() - - assert response.status_code == 200 - assert data["name"] == hero_1.name - assert data["secret_name"] == hero_1.secret_name - assert data["age"] == hero_1.age - assert data["id"] == hero_1.id - - -def test_update_hero(session: Session, client: TestClient): - hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") - session.add(hero_1) - session.commit() - - response = client.patch(f"/heroes/{hero_1.id}", json={"name": "Deadpuddle"}) - data = response.json() - - assert response.status_code == 200 - assert data["name"] == "Deadpuddle" - assert data["secret_name"] == "Dive Wilson" - assert data["age"] is None - assert data["id"] == hero_1.id - - -def test_delete_hero(session: Session, client: TestClient): - hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") - session.add(hero_1) - session.commit() - - response = client.delete(f"/heroes/{hero_1.id}") - - hero_in_db = session.get(Hero, hero_1.id) - - assert response.status_code == 200 - - assert hero_in_db is None diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_001.py b/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_001.py deleted file mode 100644 index 3ae40773f9..0000000000 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_001.py +++ /dev/null @@ -1,32 +0,0 @@ -from fastapi.testclient import TestClient -from sqlmodel import Session, SQLModel, create_engine - -from .main import app, get_session # (1)! - - -def test_create_hero(): - engine = create_engine( - "sqlite:///testing.db", connect_args={"check_same_thread": False} - ) - SQLModel.metadata.create_all(engine) - - with Session(engine) as session: - - def get_session_override(): - return session - - app.dependency_overrides[get_session] = get_session_override - - client = TestClient(app) # (2)! - - response = client.post( # (3)! - "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} - ) - app.dependency_overrides.clear() - data = response.json() # (4)! - - assert response.status_code == 200 # (5)! - assert data["name"] == "Deadpond" # (6)! - assert data["secret_name"] == "Dive Wilson" # (7)! - assert data["age"] is None # (8)! - assert data["id"] is not None # (9)! diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_002.py b/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_002.py deleted file mode 100644 index 727580b68f..0000000000 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_002.py +++ /dev/null @@ -1,32 +0,0 @@ -from fastapi.testclient import TestClient -from sqlmodel import Session, SQLModel, create_engine - -from .main import app, get_session # (1)! - - -def test_create_hero(): - engine = create_engine( - "sqlite:///testing.db", connect_args={"check_same_thread": False} - ) - SQLModel.metadata.create_all(engine) - - with Session(engine) as session: - - def get_session_override(): # (2)! - return session # (3)! - - app.dependency_overrides[get_session] = get_session_override # (4)! - - client = TestClient(app) - - response = client.post( - "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} - ) - app.dependency_overrides.clear() # (5)! - data = response.json() - - assert response.status_code == 200 - assert data["name"] == "Deadpond" - assert data["secret_name"] == "Dive Wilson" - assert data["age"] is None - assert data["id"] is not None diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_003.py b/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_003.py deleted file mode 100644 index 465c525108..0000000000 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_003.py +++ /dev/null @@ -1,33 +0,0 @@ -from fastapi.testclient import TestClient -from sqlmodel import Session, SQLModel, create_engine - -from .main import app, get_session # (1)! - - -def test_create_hero(): - engine = create_engine( # (2)! - "sqlite:///testing.db", connect_args={"check_same_thread": False} - ) - SQLModel.metadata.create_all(engine) # (3)! - - with Session(engine) as session: # (4)! - - def get_session_override(): - return session # (5)! - - app.dependency_overrides[get_session] = get_session_override # (4)! - - client = TestClient(app) - - response = client.post( - "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} - ) - app.dependency_overrides.clear() - data = response.json() - - assert response.status_code == 200 - assert data["name"] == "Deadpond" - assert data["secret_name"] == "Dive Wilson" - assert data["age"] is None - assert data["id"] is not None - # (6)! diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_004.py b/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_004.py deleted file mode 100644 index b770a9aa59..0000000000 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_004.py +++ /dev/null @@ -1,35 +0,0 @@ -from fastapi.testclient import TestClient -from sqlmodel import Session, SQLModel, create_engine -from sqlmodel.pool import StaticPool # (1)! - -from .main import app, get_session - - -def test_create_hero(): - engine = create_engine( - "sqlite://", # (2)! - connect_args={"check_same_thread": False}, - poolclass=StaticPool, # (3)! - ) - SQLModel.metadata.create_all(engine) - - with Session(engine) as session: - - def get_session_override(): - return session - - app.dependency_overrides[get_session] = get_session_override - - client = TestClient(app) - - response = client.post( - "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} - ) - app.dependency_overrides.clear() - data = response.json() - - assert response.status_code == 200 - assert data["name"] == "Deadpond" - assert data["secret_name"] == "Dive Wilson" - assert data["age"] is None - assert data["id"] is not None diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_005.py b/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_005.py deleted file mode 100644 index f653eef7ec..0000000000 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_005.py +++ /dev/null @@ -1,37 +0,0 @@ -import pytest # (1)! -from fastapi.testclient import TestClient -from sqlmodel import Session, SQLModel, create_engine -from sqlmodel.pool import StaticPool - -from .main import app, get_session - - -@pytest.fixture(name="session") # (2)! -def session_fixture(): # (3)! - engine = create_engine( - "sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool - ) - SQLModel.metadata.create_all(engine) - with Session(engine) as session: - yield session # (4)! - - -def test_create_hero(session: Session): # (5)! - def get_session_override(): - return session # (6)! - - app.dependency_overrides[get_session] = get_session_override - - client = TestClient(app) - - response = client.post( - "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} - ) - app.dependency_overrides.clear() - data = response.json() - - assert response.status_code == 200 - assert data["name"] == "Deadpond" - assert data["secret_name"] == "Dive Wilson" - assert data["age"] is None - assert data["id"] is not None diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_006.py b/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_006.py deleted file mode 100644 index 8dbfd45caf..0000000000 --- a/docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_006.py +++ /dev/null @@ -1,41 +0,0 @@ -import pytest -from fastapi.testclient import TestClient -from sqlmodel import Session, SQLModel, create_engine -from sqlmodel.pool import StaticPool - -from .main import app, get_session - - -@pytest.fixture(name="session") -def session_fixture(): - engine = create_engine( - "sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool - ) - SQLModel.metadata.create_all(engine) - with Session(engine) as session: - yield session - - -@pytest.fixture(name="client") # (1)! -def client_fixture(session: Session): # (2)! - def get_session_override(): # (3)! - return session - - app.dependency_overrides[get_session] = get_session_override # (4)! - - client = TestClient(app) # (5)! - yield client # (6)! - app.dependency_overrides.clear() # (7)! - - -def test_create_hero(client: TestClient): # (8)! - response = client.post( - "/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"} - ) - data = response.json() - - assert response.status_code == 200 - assert data["name"] == "Deadpond" - assert data["secret_name"] == "Dive Wilson" - assert data["age"] is None - assert data["id"] is not None diff --git a/docs_src/tutorial/fastapi/delete/tutorial001.py b/docs_src/tutorial/fastapi/delete/tutorial001.py deleted file mode 100644 index 977882c4c2..0000000000 --- a/docs_src/tutorial/fastapi/delete/tutorial001.py +++ /dev/null @@ -1,98 +0,0 @@ -from typing import List, Optional - -from fastapi import FastAPI, HTTPException, Query -from sqlmodel import Field, Session, SQLModel, create_engine, select - - -class HeroBase(SQLModel): - name: str = Field(index=True) - secret_name: str - age: Optional[int] = Field(default=None, index=True) - - -class Hero(HeroBase, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - - -class HeroCreate(HeroBase): - pass - - -class HeroPublic(HeroBase): - id: int - - -class HeroUpdate(SQLModel): - name: Optional[str] = None - secret_name: Optional[str] = None - age: Optional[int] = None - - -sqlite_file_name = "database.db" -sqlite_url = f"sqlite:///{sqlite_file_name}" - -connect_args = {"check_same_thread": False} -engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) - - -def create_db_and_tables(): - SQLModel.metadata.create_all(engine) - - -app = FastAPI() - - -@app.on_event("startup") -def on_startup(): - create_db_and_tables() - - -@app.post("/heroes/", response_model=HeroPublic) -def create_hero(hero: HeroCreate): - with Session(engine) as session: - db_hero = Hero.model_validate(hero) - session.add(db_hero) - session.commit() - session.refresh(db_hero) - return db_hero - - -@app.get("/heroes/", response_model=List[HeroPublic]) -def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): - with Session(engine) as session: - heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() - return heroes - - -@app.get("/heroes/{hero_id}", response_model=HeroPublic) -def read_hero(hero_id: int): - with Session(engine) as session: - hero = session.get(Hero, hero_id) - if not hero: - raise HTTPException(status_code=404, detail="Hero not found") - return hero - - -@app.patch("/heroes/{hero_id}", response_model=HeroPublic) -def update_hero(hero_id: int, hero: HeroUpdate): - with Session(engine) as session: - db_hero = session.get(Hero, hero_id) - if not db_hero: - raise HTTPException(status_code=404, detail="Hero not found") - hero_data = hero.model_dump(exclude_unset=True) - db_hero.sqlmodel_update(hero_data) - session.add(db_hero) - session.commit() - session.refresh(db_hero) - return db_hero - - -@app.delete("/heroes/{hero_id}") -def delete_hero(hero_id: int): - with Session(engine) as session: - hero = session.get(Hero, hero_id) - if not hero: - raise HTTPException(status_code=404, detail="Hero not found") - session.delete(hero) - session.commit() - return {"ok": True} diff --git a/docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py b/docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py deleted file mode 100644 index ccf3d77036..0000000000 --- a/docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py +++ /dev/null @@ -1,67 +0,0 @@ -from typing import List, Optional - -from fastapi import FastAPI, HTTPException, Query -from sqlmodel import Field, Session, SQLModel, create_engine, select - - -class HeroBase(SQLModel): - name: str = Field(index=True) - secret_name: str - age: Optional[int] = Field(default=None, index=True) - - -class Hero(HeroBase, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - - -class HeroCreate(HeroBase): - pass - - -class HeroPublic(HeroBase): - id: int - - -sqlite_file_name = "database.db" -sqlite_url = f"sqlite:///{sqlite_file_name}" - -connect_args = {"check_same_thread": False} -engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) - - -def create_db_and_tables(): - SQLModel.metadata.create_all(engine) - - -app = FastAPI() - - -@app.on_event("startup") -def on_startup(): - create_db_and_tables() - - -@app.post("/heroes/", response_model=HeroPublic) -def create_hero(hero: HeroCreate): - with Session(engine) as session: - db_hero = Hero.model_validate(hero) - session.add(db_hero) - session.commit() - session.refresh(db_hero) - return db_hero - - -@app.get("/heroes/", response_model=List[HeroPublic]) -def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): - with Session(engine) as session: - heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() - return heroes - - -@app.get("/heroes/{hero_id}", response_model=HeroPublic) -def read_hero(hero_id: int): - with Session(engine) as session: - hero = session.get(Hero, hero_id) - if not hero: - raise HTTPException(status_code=404, detail="Hero not found") - return hero diff --git a/docs_src/tutorial/fastapi/multiple_models/tutorial001.py b/docs_src/tutorial/fastapi/multiple_models/tutorial001.py deleted file mode 100644 index 42ac8051b1..0000000000 --- a/docs_src/tutorial/fastapi/multiple_models/tutorial001.py +++ /dev/null @@ -1,60 +0,0 @@ -from typing import List, Optional - -from fastapi import FastAPI -from sqlmodel import Field, Session, SQLModel, create_engine, select - - -class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - secret_name: str - age: Optional[int] = Field(default=None, index=True) - - -class HeroCreate(SQLModel): - name: str - secret_name: str - age: Optional[int] = None - - -class HeroPublic(SQLModel): - id: int - name: str - secret_name: str - age: Optional[int] = None - - -sqlite_file_name = "database.db" -sqlite_url = f"sqlite:///{sqlite_file_name}" - -connect_args = {"check_same_thread": False} -engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) - - -def create_db_and_tables(): - SQLModel.metadata.create_all(engine) - - -app = FastAPI() - - -@app.on_event("startup") -def on_startup(): - create_db_and_tables() - - -@app.post("/heroes/", response_model=HeroPublic) -def create_hero(hero: HeroCreate): - with Session(engine) as session: - db_hero = Hero.model_validate(hero) - session.add(db_hero) - session.commit() - session.refresh(db_hero) - return db_hero - - -@app.get("/heroes/", response_model=List[HeroPublic]) -def read_heroes(): - with Session(engine) as session: - heroes = session.exec(select(Hero)).all() - return heroes diff --git a/docs_src/tutorial/fastapi/multiple_models/tutorial002.py b/docs_src/tutorial/fastapi/multiple_models/tutorial002.py deleted file mode 100644 index 79c71f1a2e..0000000000 --- a/docs_src/tutorial/fastapi/multiple_models/tutorial002.py +++ /dev/null @@ -1,58 +0,0 @@ -from typing import List, Optional - -from fastapi import FastAPI -from sqlmodel import Field, Session, SQLModel, create_engine, select - - -class HeroBase(SQLModel): - name: str = Field(index=True) - secret_name: str - age: Optional[int] = Field(default=None, index=True) - - -class Hero(HeroBase, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - - -class HeroCreate(HeroBase): - pass - - -class HeroPublic(HeroBase): - id: int - - -sqlite_file_name = "database.db" -sqlite_url = f"sqlite:///{sqlite_file_name}" - -connect_args = {"check_same_thread": False} -engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) - - -def create_db_and_tables(): - SQLModel.metadata.create_all(engine) - - -app = FastAPI() - - -@app.on_event("startup") -def on_startup(): - create_db_and_tables() - - -@app.post("/heroes/", response_model=HeroPublic) -def create_hero(hero: HeroCreate): - with Session(engine) as session: - db_hero = Hero.model_validate(hero) - session.add(db_hero) - session.commit() - session.refresh(db_hero) - return db_hero - - -@app.get("/heroes/", response_model=List[HeroPublic]) -def read_heroes(): - with Session(engine) as session: - heroes = session.exec(select(Hero)).all() - return heroes diff --git a/docs_src/tutorial/fastapi/read_one/tutorial001.py b/docs_src/tutorial/fastapi/read_one/tutorial001.py deleted file mode 100644 index a39945173b..0000000000 --- a/docs_src/tutorial/fastapi/read_one/tutorial001.py +++ /dev/null @@ -1,67 +0,0 @@ -from typing import List, Optional - -from fastapi import FastAPI, HTTPException -from sqlmodel import Field, Session, SQLModel, create_engine, select - - -class HeroBase(SQLModel): - name: str = Field(index=True) - secret_name: str - age: Optional[int] = Field(default=None, index=True) - - -class Hero(HeroBase, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - - -class HeroCreate(HeroBase): - pass - - -class HeroPublic(HeroBase): - id: int - - -sqlite_file_name = "database.db" -sqlite_url = f"sqlite:///{sqlite_file_name}" - -connect_args = {"check_same_thread": False} -engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) - - -def create_db_and_tables(): - SQLModel.metadata.create_all(engine) - - -app = FastAPI() - - -@app.on_event("startup") -def on_startup(): - create_db_and_tables() - - -@app.post("/heroes/", response_model=HeroPublic) -def create_hero(hero: HeroCreate): - with Session(engine) as session: - db_hero = Hero.model_validate(hero) - session.add(db_hero) - session.commit() - session.refresh(db_hero) - return db_hero - - -@app.get("/heroes/", response_model=List[HeroPublic]) -def read_heroes(): - with Session(engine) as session: - heroes = session.exec(select(Hero)).all() - return heroes - - -@app.get("/heroes/{hero_id}", response_model=HeroPublic) -def read_hero(hero_id: int): - with Session(engine) as session: - hero = session.get(Hero, hero_id) - if not hero: - raise HTTPException(status_code=404, detail="Hero not found") - return hero diff --git a/docs_src/tutorial/fastapi/relationships/tutorial001.py b/docs_src/tutorial/fastapi/relationships/tutorial001.py deleted file mode 100644 index 59b44730ba..0000000000 --- a/docs_src/tutorial/fastapi/relationships/tutorial001.py +++ /dev/null @@ -1,199 +0,0 @@ -from typing import List, Optional - -from fastapi import Depends, FastAPI, HTTPException, Query -from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select - - -class TeamBase(SQLModel): - name: str = Field(index=True) - headquarters: str - - -class Team(TeamBase, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - - heroes: List["Hero"] = Relationship(back_populates="team") - - -class TeamCreate(TeamBase): - pass - - -class TeamPublic(TeamBase): - id: int - - -class TeamUpdate(SQLModel): - id: Optional[int] = None - name: Optional[str] = None - headquarters: Optional[str] = None - - -class HeroBase(SQLModel): - name: str = Field(index=True) - secret_name: str - age: Optional[int] = Field(default=None, index=True) - - team_id: Optional[int] = Field(default=None, foreign_key="team.id") - - -class Hero(HeroBase, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - - team: Optional[Team] = Relationship(back_populates="heroes") - - -class HeroPublic(HeroBase): - id: int - - -class HeroCreate(HeroBase): - pass - - -class HeroUpdate(SQLModel): - name: Optional[str] = None - secret_name: Optional[str] = None - age: Optional[int] = None - team_id: Optional[int] = None - - -class HeroPublicWithTeam(HeroPublic): - team: Optional[TeamPublic] = None - - -class TeamPublicWithHeroes(TeamPublic): - heroes: List[HeroPublic] = [] - - -sqlite_file_name = "database.db" -sqlite_url = f"sqlite:///{sqlite_file_name}" - -connect_args = {"check_same_thread": False} -engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) - - -def create_db_and_tables(): - SQLModel.metadata.create_all(engine) - - -def get_session(): - with Session(engine) as session: - yield session - - -app = FastAPI() - - -@app.on_event("startup") -def on_startup(): - create_db_and_tables() - - -@app.post("/heroes/", response_model=HeroPublic) -def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): - db_hero = Hero.model_validate(hero) - session.add(db_hero) - session.commit() - session.refresh(db_hero) - return db_hero - - -@app.get("/heroes/", response_model=List[HeroPublic]) -def read_heroes( - *, - session: Session = Depends(get_session), - offset: int = 0, - limit: int = Query(default=100, le=100), -): - heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() - return heroes - - -@app.get("/heroes/{hero_id}", response_model=HeroPublicWithTeam) -def read_hero(*, session: Session = Depends(get_session), hero_id: int): - hero = session.get(Hero, hero_id) - if not hero: - raise HTTPException(status_code=404, detail="Hero not found") - return hero - - -@app.patch("/heroes/{hero_id}", response_model=HeroPublic) -def update_hero( - *, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate -): - db_hero = session.get(Hero, hero_id) - if not db_hero: - raise HTTPException(status_code=404, detail="Hero not found") - hero_data = hero.model_dump(exclude_unset=True) - db_hero.sqlmodel_update(hero_data) - session.add(db_hero) - session.commit() - session.refresh(db_hero) - return db_hero - - -@app.delete("/heroes/{hero_id}") -def delete_hero(*, session: Session = Depends(get_session), hero_id: int): - hero = session.get(Hero, hero_id) - if not hero: - raise HTTPException(status_code=404, detail="Hero not found") - session.delete(hero) - session.commit() - return {"ok": True} - - -@app.post("/teams/", response_model=TeamPublic) -def create_team(*, session: Session = Depends(get_session), team: TeamCreate): - db_team = Team.model_validate(team) - session.add(db_team) - session.commit() - session.refresh(db_team) - return db_team - - -@app.get("/teams/", response_model=List[TeamPublic]) -def read_teams( - *, - session: Session = Depends(get_session), - offset: int = 0, - limit: int = Query(default=100, le=100), -): - teams = session.exec(select(Team).offset(offset).limit(limit)).all() - return teams - - -@app.get("/teams/{team_id}", response_model=TeamPublicWithHeroes) -def read_team(*, team_id: int, session: Session = Depends(get_session)): - team = session.get(Team, team_id) - if not team: - raise HTTPException(status_code=404, detail="Team not found") - return team - - -@app.patch("/teams/{team_id}", response_model=TeamPublic) -def update_team( - *, - session: Session = Depends(get_session), - team_id: int, - team: TeamUpdate, -): - db_team = session.get(Team, team_id) - if not db_team: - raise HTTPException(status_code=404, detail="Team not found") - team_data = team.model_dump(exclude_unset=True) - db_team.sqlmodel_update(team_data) - session.add(db_team) - session.commit() - session.refresh(db_team) - return db_team - - -@app.delete("/teams/{team_id}") -def delete_team(*, session: Session = Depends(get_session), team_id: int): - team = session.get(Team, team_id) - if not team: - raise HTTPException(status_code=404, detail="Team not found") - session.delete(team) - session.commit() - return {"ok": True} diff --git a/docs_src/tutorial/fastapi/response_model/tutorial001.py b/docs_src/tutorial/fastapi/response_model/tutorial001.py deleted file mode 100644 index 57d8738395..0000000000 --- a/docs_src/tutorial/fastapi/response_model/tutorial001.py +++ /dev/null @@ -1,46 +0,0 @@ -from typing import List, Optional - -from fastapi import FastAPI -from sqlmodel import Field, Session, SQLModel, create_engine, select - - -class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - secret_name: str - age: Optional[int] = Field(default=None, index=True) - - -sqlite_file_name = "database.db" -sqlite_url = f"sqlite:///{sqlite_file_name}" - -connect_args = {"check_same_thread": False} -engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) - - -def create_db_and_tables(): - SQLModel.metadata.create_all(engine) - - -app = FastAPI() - - -@app.on_event("startup") -def on_startup(): - create_db_and_tables() - - -@app.post("/heroes/", response_model=Hero) -def create_hero(hero: Hero): - with Session(engine) as session: - session.add(hero) - session.commit() - session.refresh(hero) - return hero - - -@app.get("/heroes/", response_model=List[Hero]) -def read_heroes(): - with Session(engine) as session: - heroes = session.exec(select(Hero)).all() - return heroes diff --git a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py b/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py deleted file mode 100644 index f0a2559467..0000000000 --- a/docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py +++ /dev/null @@ -1,105 +0,0 @@ -from typing import List, Optional - -from fastapi import Depends, FastAPI, HTTPException, Query -from sqlmodel import Field, Session, SQLModel, create_engine, select - - -class HeroBase(SQLModel): - name: str = Field(index=True) - secret_name: str - age: Optional[int] = Field(default=None, index=True) - - -class Hero(HeroBase, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - - -class HeroCreate(HeroBase): - pass - - -class HeroPublic(HeroBase): - id: int - - -class HeroUpdate(SQLModel): - name: Optional[str] = None - secret_name: Optional[str] = None - age: Optional[int] = None - - -sqlite_file_name = "database.db" -sqlite_url = f"sqlite:///{sqlite_file_name}" - -connect_args = {"check_same_thread": False} -engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) - - -def create_db_and_tables(): - SQLModel.metadata.create_all(engine) - - -def get_session(): - with Session(engine) as session: - yield session - - -app = FastAPI() - - -@app.on_event("startup") -def on_startup(): - create_db_and_tables() - - -@app.post("/heroes/", response_model=HeroPublic) -def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): - db_hero = Hero.model_validate(hero) - session.add(db_hero) - session.commit() - session.refresh(db_hero) - return db_hero - - -@app.get("/heroes/", response_model=List[HeroPublic]) -def read_heroes( - *, - session: Session = Depends(get_session), - offset: int = 0, - limit: int = Query(default=100, le=100), -): - heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() - return heroes - - -@app.get("/heroes/{hero_id}", response_model=HeroPublic) -def read_hero(*, session: Session = Depends(get_session), hero_id: int): - hero = session.get(Hero, hero_id) - if not hero: - raise HTTPException(status_code=404, detail="Hero not found") - return hero - - -@app.patch("/heroes/{hero_id}", response_model=HeroPublic) -def update_hero( - *, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate -): - db_hero = session.get(Hero, hero_id) - if not db_hero: - raise HTTPException(status_code=404, detail="Hero not found") - hero_data = hero.model_dump(exclude_unset=True) - db_hero.sqlmodel_update(hero_data) - session.add(db_hero) - session.commit() - session.refresh(db_hero) - return db_hero - - -@app.delete("/heroes/{hero_id}") -def delete_hero(*, session: Session = Depends(get_session), hero_id: int): - hero = session.get(Hero, hero_id) - if not hero: - raise HTTPException(status_code=404, detail="Hero not found") - session.delete(hero) - session.commit() - return {"ok": True} diff --git a/docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py b/docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py39.py similarity index 100% rename from docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py rename to docs_src/tutorial/fastapi/simple_hero_api/tutorial001_py39.py diff --git a/docs_src/tutorial/fastapi/teams/tutorial001.py b/docs_src/tutorial/fastapi/teams/tutorial001.py deleted file mode 100644 index 49dd83065a..0000000000 --- a/docs_src/tutorial/fastapi/teams/tutorial001.py +++ /dev/null @@ -1,190 +0,0 @@ -from typing import List, Optional - -from fastapi import Depends, FastAPI, HTTPException, Query -from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select - - -class TeamBase(SQLModel): - name: str = Field(index=True) - headquarters: str - - -class Team(TeamBase, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - - heroes: List["Hero"] = Relationship(back_populates="team") - - -class TeamCreate(TeamBase): - pass - - -class TeamPublic(TeamBase): - id: int - - -class TeamUpdate(SQLModel): - name: Optional[str] = None - headquarters: Optional[str] = None - - -class HeroBase(SQLModel): - name: str = Field(index=True) - secret_name: str - age: Optional[int] = Field(default=None, index=True) - - team_id: Optional[int] = Field(default=None, foreign_key="team.id") - - -class Hero(HeroBase, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - - team: Optional[Team] = Relationship(back_populates="heroes") - - -class HeroPublic(HeroBase): - id: int - - -class HeroCreate(HeroBase): - pass - - -class HeroUpdate(SQLModel): - name: Optional[str] = None - secret_name: Optional[str] = None - age: Optional[int] = None - team_id: Optional[int] = None - - -sqlite_file_name = "database.db" -sqlite_url = f"sqlite:///{sqlite_file_name}" - -connect_args = {"check_same_thread": False} -engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) - - -def create_db_and_tables(): - SQLModel.metadata.create_all(engine) - - -def get_session(): - with Session(engine) as session: - yield session - - -app = FastAPI() - - -@app.on_event("startup") -def on_startup(): - create_db_and_tables() - - -@app.post("/heroes/", response_model=HeroPublic) -def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate): - db_hero = Hero.model_validate(hero) - session.add(db_hero) - session.commit() - session.refresh(db_hero) - return db_hero - - -@app.get("/heroes/", response_model=List[HeroPublic]) -def read_heroes( - *, - session: Session = Depends(get_session), - offset: int = 0, - limit: int = Query(default=100, le=100), -): - heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() - return heroes - - -@app.get("/heroes/{hero_id}", response_model=HeroPublic) -def read_hero(*, session: Session = Depends(get_session), hero_id: int): - hero = session.get(Hero, hero_id) - if not hero: - raise HTTPException(status_code=404, detail="Hero not found") - return hero - - -@app.patch("/heroes/{hero_id}", response_model=HeroPublic) -def update_hero( - *, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate -): - db_hero = session.get(Hero, hero_id) - if not db_hero: - raise HTTPException(status_code=404, detail="Hero not found") - hero_data = hero.model_dump(exclude_unset=True) - db_hero.sqlmodel_update(hero_data) - session.add(db_hero) - session.commit() - session.refresh(db_hero) - return db_hero - - -@app.delete("/heroes/{hero_id}") -def delete_hero(*, session: Session = Depends(get_session), hero_id: int): - hero = session.get(Hero, hero_id) - if not hero: - raise HTTPException(status_code=404, detail="Hero not found") - session.delete(hero) - session.commit() - return {"ok": True} - - -@app.post("/teams/", response_model=TeamPublic) -def create_team(*, session: Session = Depends(get_session), team: TeamCreate): - db_team = Team.model_validate(team) - session.add(db_team) - session.commit() - session.refresh(db_team) - return db_team - - -@app.get("/teams/", response_model=List[TeamPublic]) -def read_teams( - *, - session: Session = Depends(get_session), - offset: int = 0, - limit: int = Query(default=100, le=100), -): - teams = session.exec(select(Team).offset(offset).limit(limit)).all() - return teams - - -@app.get("/teams/{team_id}", response_model=TeamPublic) -def read_team(*, team_id: int, session: Session = Depends(get_session)): - team = session.get(Team, team_id) - if not team: - raise HTTPException(status_code=404, detail="Team not found") - return team - - -@app.patch("/teams/{team_id}", response_model=TeamPublic) -def update_team( - *, - session: Session = Depends(get_session), - team_id: int, - team: TeamUpdate, -): - db_team = session.get(Team, team_id) - if not db_team: - raise HTTPException(status_code=404, detail="Team not found") - team_data = team.model_dump(exclude_unset=True) - db_team.sqlmodel_update(team_data) - session.add(db_team) - session.commit() - session.refresh(db_team) - return db_team - - -@app.delete("/teams/{team_id}") -def delete_team(*, session: Session = Depends(get_session), team_id: int): - team = session.get(Team, team_id) - if not team: - raise HTTPException(status_code=404, detail="Team not found") - session.delete(team) - session.commit() - return {"ok": True} diff --git a/docs_src/tutorial/fastapi/update/tutorial001.py b/docs_src/tutorial/fastapi/update/tutorial001.py deleted file mode 100644 index 6a02f6c015..0000000000 --- a/docs_src/tutorial/fastapi/update/tutorial001.py +++ /dev/null @@ -1,87 +0,0 @@ -from typing import List, Optional - -from fastapi import FastAPI, HTTPException, Query -from sqlmodel import Field, Session, SQLModel, create_engine, select - - -class HeroBase(SQLModel): - name: str = Field(index=True) - secret_name: str - age: Optional[int] = Field(default=None, index=True) - - -class Hero(HeroBase, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - - -class HeroCreate(HeroBase): - pass - - -class HeroPublic(HeroBase): - id: int - - -class HeroUpdate(SQLModel): - name: Optional[str] = None - secret_name: Optional[str] = None - age: Optional[int] = None - - -sqlite_file_name = "database.db" -sqlite_url = f"sqlite:///{sqlite_file_name}" - -connect_args = {"check_same_thread": False} -engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) - - -def create_db_and_tables(): - SQLModel.metadata.create_all(engine) - - -app = FastAPI() - - -@app.on_event("startup") -def on_startup(): - create_db_and_tables() - - -@app.post("/heroes/", response_model=HeroPublic) -def create_hero(hero: HeroCreate): - with Session(engine) as session: - db_hero = Hero.model_validate(hero) - session.add(db_hero) - session.commit() - session.refresh(db_hero) - return db_hero - - -@app.get("/heroes/", response_model=List[HeroPublic]) -def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): - with Session(engine) as session: - heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() - return heroes - - -@app.get("/heroes/{hero_id}", response_model=HeroPublic) -def read_hero(hero_id: int): - with Session(engine) as session: - hero = session.get(Hero, hero_id) - if not hero: - raise HTTPException(status_code=404, detail="Hero not found") - return hero - - -@app.patch("/heroes/{hero_id}", response_model=HeroPublic) -def update_hero(hero_id: int, hero: HeroUpdate): - with Session(engine) as session: - db_hero = session.get(Hero, hero_id) - if not db_hero: - raise HTTPException(status_code=404, detail="Hero not found") - hero_data = hero.model_dump(exclude_unset=True) - db_hero.sqlmodel_update(hero_data) - session.add(db_hero) - session.commit() - session.refresh(db_hero) - return db_hero diff --git a/docs_src/tutorial/fastapi/update/tutorial002.py b/docs_src/tutorial/fastapi/update/tutorial002.py deleted file mode 100644 index c8838aeffb..0000000000 --- a/docs_src/tutorial/fastapi/update/tutorial002.py +++ /dev/null @@ -1,101 +0,0 @@ -from typing import List, Optional - -from fastapi import FastAPI, HTTPException, Query -from sqlmodel import Field, Session, SQLModel, create_engine, select - - -class HeroBase(SQLModel): - name: str = Field(index=True) - secret_name: str - age: Optional[int] = Field(default=None, index=True) - - -class Hero(HeroBase, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - hashed_password: str = Field() - - -class HeroCreate(HeroBase): - password: str - - -class HeroPublic(HeroBase): - id: int - - -class HeroUpdate(SQLModel): - name: Optional[str] = None - secret_name: Optional[str] = None - age: Optional[int] = None - password: Optional[str] = None - - -sqlite_file_name = "database.db" -sqlite_url = f"sqlite:///{sqlite_file_name}" - -connect_args = {"check_same_thread": False} -engine = create_engine(sqlite_url, echo=True, connect_args=connect_args) - - -def create_db_and_tables(): - SQLModel.metadata.create_all(engine) - - -def hash_password(password: str) -> str: - # Use something like passlib here - return f"not really hashed {password} hehehe" - - -app = FastAPI() - - -@app.on_event("startup") -def on_startup(): - create_db_and_tables() - - -@app.post("/heroes/", response_model=HeroPublic) -def create_hero(hero: HeroCreate): - hashed_password = hash_password(hero.password) - with Session(engine) as session: - extra_data = {"hashed_password": hashed_password} - db_hero = Hero.model_validate(hero, update=extra_data) - session.add(db_hero) - session.commit() - session.refresh(db_hero) - return db_hero - - -@app.get("/heroes/", response_model=List[HeroPublic]) -def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)): - with Session(engine) as session: - heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() - return heroes - - -@app.get("/heroes/{hero_id}", response_model=HeroPublic) -def read_hero(hero_id: int): - with Session(engine) as session: - hero = session.get(Hero, hero_id) - if not hero: - raise HTTPException(status_code=404, detail="Hero not found") - return hero - - -@app.patch("/heroes/{hero_id}", response_model=HeroPublic) -def update_hero(hero_id: int, hero: HeroUpdate): - with Session(engine) as session: - db_hero = session.get(Hero, hero_id) - if not db_hero: - raise HTTPException(status_code=404, detail="Hero not found") - hero_data = hero.model_dump(exclude_unset=True) - extra_data = {} - if "password" in hero_data: - password = hero_data["password"] - hashed_password = hash_password(password) - extra_data["hashed_password"] = hashed_password - db_hero.sqlmodel_update(hero_data, update=extra_data) - session.add(db_hero) - session.commit() - session.refresh(db_hero) - return db_hero diff --git a/docs_src/tutorial/indexes/tutorial001.py b/docs_src/tutorial/indexes/tutorial001_py39.py similarity index 100% rename from docs_src/tutorial/indexes/tutorial001.py rename to docs_src/tutorial/indexes/tutorial001_py39.py diff --git a/docs_src/tutorial/indexes/tutorial002.py b/docs_src/tutorial/indexes/tutorial002_py39.py similarity index 100% rename from docs_src/tutorial/indexes/tutorial002.py rename to docs_src/tutorial/indexes/tutorial002_py39.py diff --git a/docs_src/tutorial/insert/tutorial001.py b/docs_src/tutorial/insert/tutorial001_py39.py similarity index 100% rename from docs_src/tutorial/insert/tutorial001.py rename to docs_src/tutorial/insert/tutorial001_py39.py diff --git a/docs_src/tutorial/insert/tutorial002.py b/docs_src/tutorial/insert/tutorial002_py39.py similarity index 100% rename from docs_src/tutorial/insert/tutorial002.py rename to docs_src/tutorial/insert/tutorial002_py39.py diff --git a/docs_src/tutorial/insert/tutorial003.py b/docs_src/tutorial/insert/tutorial003_py39.py similarity index 100% rename from docs_src/tutorial/insert/tutorial003.py rename to docs_src/tutorial/insert/tutorial003_py39.py diff --git a/docs_src/tutorial/many_to_many/tutorial001.py b/docs_src/tutorial/many_to_many/tutorial001.py deleted file mode 100644 index 79f9e7909a..0000000000 --- a/docs_src/tutorial/many_to_many/tutorial001.py +++ /dev/null @@ -1,84 +0,0 @@ -from typing import List, Optional - -from sqlmodel import Field, Relationship, Session, SQLModel, create_engine - - -class HeroTeamLink(SQLModel, table=True): - team_id: Optional[int] = Field( - default=None, foreign_key="team.id", primary_key=True - ) - hero_id: Optional[int] = Field( - default=None, foreign_key="hero.id", primary_key=True - ) - - -class Team(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - headquarters: str - - heroes: List["Hero"] = Relationship(back_populates="teams", link_model=HeroTeamLink) - - -class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - secret_name: str - age: Optional[int] = Field(default=None, index=True) - - teams: List[Team] = Relationship(back_populates="heroes", link_model=HeroTeamLink) - - -sqlite_file_name = "database.db" -sqlite_url = f"sqlite:///{sqlite_file_name}" - -engine = create_engine(sqlite_url, echo=True) - - -def create_db_and_tables(): - SQLModel.metadata.create_all(engine) - - -def create_heroes(): - with Session(engine) as session: - team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") - - hero_deadpond = Hero( - name="Deadpond", - secret_name="Dive Wilson", - teams=[team_z_force, team_preventers], - ) - hero_rusty_man = Hero( - name="Rusty-Man", - secret_name="Tommy Sharp", - age=48, - teams=[team_preventers], - ) - hero_spider_boy = Hero( - name="Spider-Boy", secret_name="Pedro Parqueador", teams=[team_preventers] - ) - session.add(hero_deadpond) - session.add(hero_rusty_man) - session.add(hero_spider_boy) - session.commit() - - session.refresh(hero_deadpond) - session.refresh(hero_rusty_man) - session.refresh(hero_spider_boy) - - print("Deadpond:", hero_deadpond) - print("Deadpond teams:", hero_deadpond.teams) - print("Rusty-Man:", hero_rusty_man) - print("Rusty-Man Teams:", hero_rusty_man.teams) - print("Spider-Boy:", hero_spider_boy) - print("Spider-Boy Teams:", hero_spider_boy.teams) - - -def main(): - create_db_and_tables() - create_heroes() - - -if __name__ == "__main__": - main() diff --git a/docs_src/tutorial/many_to_many/tutorial002.py b/docs_src/tutorial/many_to_many/tutorial002.py deleted file mode 100644 index 9845340ec4..0000000000 --- a/docs_src/tutorial/many_to_many/tutorial002.py +++ /dev/null @@ -1,107 +0,0 @@ -from typing import List, Optional - -from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select - - -class HeroTeamLink(SQLModel, table=True): - team_id: Optional[int] = Field( - default=None, foreign_key="team.id", primary_key=True - ) - hero_id: Optional[int] = Field( - default=None, foreign_key="hero.id", primary_key=True - ) - - -class Team(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - headquarters: str - - heroes: List["Hero"] = Relationship(back_populates="teams", link_model=HeroTeamLink) - - -class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - secret_name: str - age: Optional[int] = Field(default=None, index=True) - - teams: List[Team] = Relationship(back_populates="heroes", link_model=HeroTeamLink) - - -sqlite_file_name = "database.db" -sqlite_url = f"sqlite:///{sqlite_file_name}" - -engine = create_engine(sqlite_url, echo=True) - - -def create_db_and_tables(): - SQLModel.metadata.create_all(engine) - - -def create_heroes(): - with Session(engine) as session: - team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") - - hero_deadpond = Hero( - name="Deadpond", - secret_name="Dive Wilson", - teams=[team_z_force, team_preventers], - ) - hero_rusty_man = Hero( - name="Rusty-Man", - secret_name="Tommy Sharp", - age=48, - teams=[team_preventers], - ) - hero_spider_boy = Hero( - name="Spider-Boy", secret_name="Pedro Parqueador", teams=[team_preventers] - ) - session.add(hero_deadpond) - session.add(hero_rusty_man) - session.add(hero_spider_boy) - session.commit() - - session.refresh(hero_deadpond) - session.refresh(hero_rusty_man) - session.refresh(hero_spider_boy) - - print("Deadpond:", hero_deadpond) - print("Deadpond teams:", hero_deadpond.teams) - print("Rusty-Man:", hero_rusty_man) - print("Rusty-Man Teams:", hero_rusty_man.teams) - print("Spider-Boy:", hero_spider_boy) - print("Spider-Boy Teams:", hero_spider_boy.teams) - - -def update_heroes(): - with Session(engine) as session: - hero_spider_boy = session.exec( - select(Hero).where(Hero.name == "Spider-Boy") - ).one() - team_z_force = session.exec(select(Team).where(Team.name == "Z-Force")).one() - - team_z_force.heroes.append(hero_spider_boy) - session.add(team_z_force) - session.commit() - - print("Updated Spider-Boy's Teams:", hero_spider_boy.teams) - print("Z-Force heroes:", team_z_force.heroes) - - hero_spider_boy.teams.remove(team_z_force) - session.add(team_z_force) - session.commit() - - print("Reverted Z-Force's heroes:", team_z_force.heroes) - print("Reverted Spider-Boy's teams:", hero_spider_boy.teams) - - -def main(): - create_db_and_tables() - create_heroes() - update_heroes() - - -if __name__ == "__main__": - main() diff --git a/docs_src/tutorial/many_to_many/tutorial003.py b/docs_src/tutorial/many_to_many/tutorial003.py deleted file mode 100644 index 6c23878b01..0000000000 --- a/docs_src/tutorial/many_to_many/tutorial003.py +++ /dev/null @@ -1,123 +0,0 @@ -from typing import List, Optional - -from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select - - -class HeroTeamLink(SQLModel, table=True): - team_id: Optional[int] = Field( - default=None, foreign_key="team.id", primary_key=True - ) - hero_id: Optional[int] = Field( - default=None, foreign_key="hero.id", primary_key=True - ) - is_training: bool = False - - team: "Team" = Relationship(back_populates="hero_links") - hero: "Hero" = Relationship(back_populates="team_links") - - -class Team(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - headquarters: str - - hero_links: List[HeroTeamLink] = Relationship(back_populates="team") - - -class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - secret_name: str - age: Optional[int] = Field(default=None, index=True) - - team_links: List[HeroTeamLink] = Relationship(back_populates="hero") - - -sqlite_file_name = "database.db" -sqlite_url = f"sqlite:///{sqlite_file_name}" - -engine = create_engine(sqlite_url, echo=True) - - -def create_db_and_tables(): - SQLModel.metadata.create_all(engine) - - -def create_heroes(): - with Session(engine) as session: - team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") - - hero_deadpond = Hero( - name="Deadpond", - secret_name="Dive Wilson", - ) - hero_rusty_man = Hero( - name="Rusty-Man", - secret_name="Tommy Sharp", - age=48, - ) - hero_spider_boy = Hero( - name="Spider-Boy", - secret_name="Pedro Parqueador", - ) - deadpond_team_z_link = HeroTeamLink(team=team_z_force, hero=hero_deadpond) - deadpond_preventers_link = HeroTeamLink( - team=team_preventers, hero=hero_deadpond, is_training=True - ) - spider_boy_preventers_link = HeroTeamLink( - team=team_preventers, hero=hero_spider_boy, is_training=True - ) - rusty_man_preventers_link = HeroTeamLink( - team=team_preventers, hero=hero_rusty_man - ) - - session.add(deadpond_team_z_link) - session.add(deadpond_preventers_link) - session.add(spider_boy_preventers_link) - session.add(rusty_man_preventers_link) - session.commit() - - for link in team_z_force.hero_links: - print("Z-Force hero:", link.hero, "is training:", link.is_training) - - for link in team_preventers.hero_links: - print("Preventers hero:", link.hero, "is training:", link.is_training) - - -def update_heroes(): - with Session(engine) as session: - hero_spider_boy = session.exec( - select(Hero).where(Hero.name == "Spider-Boy") - ).one() - team_z_force = session.exec(select(Team).where(Team.name == "Z-Force")).one() - - spider_boy_z_force_link = HeroTeamLink( - team=team_z_force, hero=hero_spider_boy, is_training=True - ) - team_z_force.hero_links.append(spider_boy_z_force_link) - session.add(team_z_force) - session.commit() - - print("Updated Spider-Boy's Teams:", hero_spider_boy.team_links) - print("Z-Force heroes:", team_z_force.hero_links) - - for link in hero_spider_boy.team_links: - if link.team.name == "Preventers": - link.is_training = False - - session.add(hero_spider_boy) - session.commit() - - for link in hero_spider_boy.team_links: - print("Spider-Boy team:", link.team, "is training:", link.is_training) - - -def main(): - create_db_and_tables() - create_heroes() - update_heroes() - - -if __name__ == "__main__": - main() diff --git a/docs_src/tutorial/offset_and_limit/tutorial001.py b/docs_src/tutorial/offset_and_limit/tutorial001_py39.py similarity index 100% rename from docs_src/tutorial/offset_and_limit/tutorial001.py rename to docs_src/tutorial/offset_and_limit/tutorial001_py39.py diff --git a/docs_src/tutorial/offset_and_limit/tutorial002.py b/docs_src/tutorial/offset_and_limit/tutorial002_py39.py similarity index 100% rename from docs_src/tutorial/offset_and_limit/tutorial002.py rename to docs_src/tutorial/offset_and_limit/tutorial002_py39.py diff --git a/docs_src/tutorial/offset_and_limit/tutorial003.py b/docs_src/tutorial/offset_and_limit/tutorial003_py39.py similarity index 100% rename from docs_src/tutorial/offset_and_limit/tutorial003.py rename to docs_src/tutorial/offset_and_limit/tutorial003_py39.py diff --git a/docs_src/tutorial/offset_and_limit/tutorial004.py b/docs_src/tutorial/offset_and_limit/tutorial004_py39.py similarity index 100% rename from docs_src/tutorial/offset_and_limit/tutorial004.py rename to docs_src/tutorial/offset_and_limit/tutorial004_py39.py diff --git a/docs_src/tutorial/one/tutorial001.py b/docs_src/tutorial/one/tutorial001_py39.py similarity index 100% rename from docs_src/tutorial/one/tutorial001.py rename to docs_src/tutorial/one/tutorial001_py39.py diff --git a/docs_src/tutorial/one/tutorial002.py b/docs_src/tutorial/one/tutorial002_py39.py similarity index 100% rename from docs_src/tutorial/one/tutorial002.py rename to docs_src/tutorial/one/tutorial002_py39.py diff --git a/docs_src/tutorial/one/tutorial003.py b/docs_src/tutorial/one/tutorial003_py39.py similarity index 100% rename from docs_src/tutorial/one/tutorial003.py rename to docs_src/tutorial/one/tutorial003_py39.py diff --git a/docs_src/tutorial/one/tutorial004.py b/docs_src/tutorial/one/tutorial004_py39.py similarity index 100% rename from docs_src/tutorial/one/tutorial004.py rename to docs_src/tutorial/one/tutorial004_py39.py diff --git a/docs_src/tutorial/one/tutorial005.py b/docs_src/tutorial/one/tutorial005_py39.py similarity index 100% rename from docs_src/tutorial/one/tutorial005.py rename to docs_src/tutorial/one/tutorial005_py39.py diff --git a/docs_src/tutorial/one/tutorial006.py b/docs_src/tutorial/one/tutorial006_py39.py similarity index 100% rename from docs_src/tutorial/one/tutorial006.py rename to docs_src/tutorial/one/tutorial006_py39.py diff --git a/docs_src/tutorial/one/tutorial007.py b/docs_src/tutorial/one/tutorial007_py39.py similarity index 100% rename from docs_src/tutorial/one/tutorial007.py rename to docs_src/tutorial/one/tutorial007_py39.py diff --git a/docs_src/tutorial/one/tutorial008.py b/docs_src/tutorial/one/tutorial008_py39.py similarity index 100% rename from docs_src/tutorial/one/tutorial008.py rename to docs_src/tutorial/one/tutorial008_py39.py diff --git a/docs_src/tutorial/one/tutorial009.py b/docs_src/tutorial/one/tutorial009_py39.py similarity index 100% rename from docs_src/tutorial/one/tutorial009.py rename to docs_src/tutorial/one/tutorial009_py39.py diff --git a/docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py b/docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py deleted file mode 100644 index 39230cb697..0000000000 --- a/docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py +++ /dev/null @@ -1,143 +0,0 @@ -from typing import List, Optional - -from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select - - -class Team(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - headquarters: str - - heroes: List["Hero"] = Relationship() - - -class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - secret_name: str - age: Optional[int] = Field(default=None, index=True) - - team_id: Optional[int] = Field(default=None, foreign_key="team.id") - team: Optional[Team] = Relationship() - - -sqlite_file_name = "database.db" -sqlite_url = f"sqlite:///{sqlite_file_name}" - -engine = create_engine(sqlite_url, echo=True) - - -def create_db_and_tables(): - SQLModel.metadata.create_all(engine) - - -def create_heroes(): - with Session(engine) as session: - team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") - - hero_deadpond = Hero( - name="Deadpond", secret_name="Dive Wilson", team=team_z_force - ) - hero_rusty_man = Hero( - name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers - ) - 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) - session.commit() - - session.refresh(hero_deadpond) - session.refresh(hero_rusty_man) - 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 = team_preventers - session.add(hero_spider_boy) - session.commit() - session.refresh(hero_spider_boy) - print("Updated hero:", hero_spider_boy) - - hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) - hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") - team_wakaland = Team( - name="Wakaland", - headquarters="Wakaland Capital City", - heroes=[hero_black_lion, hero_sure_e], - ) - session.add(team_wakaland) - session.commit() - session.refresh(team_wakaland) - print("Team Wakaland:", team_wakaland) - - hero_tarantula = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) - hero_dr_weird = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) - hero_cap = Hero( - name="Captain North America", secret_name="Esteban Rogelios", age=93 - ) - - team_preventers.heroes.append(hero_tarantula) - team_preventers.heroes.append(hero_dr_weird) - team_preventers.heroes.append(hero_cap) - session.add(team_preventers) - session.commit() - session.refresh(hero_tarantula) - session.refresh(hero_dr_weird) - session.refresh(hero_cap) - print("Preventers new hero:", hero_tarantula) - print("Preventers new hero:", hero_dr_weird) - print("Preventers new hero:", hero_cap) - - -def select_heroes(): - with Session(engine) as session: - statement = select(Team).where(Team.name == "Preventers") - result = session.exec(statement) - team_preventers = result.one() - - print("Preventers heroes:", team_preventers.heroes) - - -def update_heroes(): - with Session(engine) as session: - hero_spider_boy = session.exec( - select(Hero).where(Hero.name == "Spider-Boy") - ).one() - - preventers_team = session.exec( - select(Team).where(Team.name == "Preventers") - ).one() - - print("Hero Spider-Boy:", hero_spider_boy) - print("Preventers Team:", preventers_team) - print("Preventers Team Heroes:", preventers_team.heroes) - - hero_spider_boy.team = None - - print("Spider-Boy without team:", hero_spider_boy) - - print("Preventers Team Heroes again:", preventers_team.heroes) - - session.add(hero_spider_boy) - session.commit() - print("After committing") - - session.refresh(hero_spider_boy) - print("Spider-Boy after commit:", hero_spider_boy) - - print("Preventers Team Heroes after commit:", preventers_team.heroes) - - -def main(): - create_db_and_tables() - create_heroes() - select_heroes() - update_heroes() - - -if __name__ == "__main__": - main() diff --git a/docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py b/docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py deleted file mode 100644 index a1add4bccb..0000000000 --- a/docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py +++ /dev/null @@ -1,143 +0,0 @@ -from typing import List, Optional - -from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select - - -class Team(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - headquarters: str - - heroes: List["Hero"] = Relationship(back_populates="team") - - -class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - secret_name: str - age: Optional[int] = Field(default=None, index=True) - - team_id: Optional[int] = Field(default=None, foreign_key="team.id") - team: Optional[Team] = Relationship(back_populates="heroes") - - -sqlite_file_name = "database.db" -sqlite_url = f"sqlite:///{sqlite_file_name}" - -engine = create_engine(sqlite_url, echo=True) - - -def create_db_and_tables(): - SQLModel.metadata.create_all(engine) - - -def create_heroes(): - with Session(engine) as session: - team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") - - hero_deadpond = Hero( - name="Deadpond", secret_name="Dive Wilson", team=team_z_force - ) - hero_rusty_man = Hero( - name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers - ) - 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) - session.commit() - - session.refresh(hero_deadpond) - session.refresh(hero_rusty_man) - 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 = team_preventers - session.add(hero_spider_boy) - session.commit() - session.refresh(hero_spider_boy) - print("Updated hero:", hero_spider_boy) - - hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) - hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") - team_wakaland = Team( - name="Wakaland", - headquarters="Wakaland Capital City", - heroes=[hero_black_lion, hero_sure_e], - ) - session.add(team_wakaland) - session.commit() - session.refresh(team_wakaland) - print("Team Wakaland:", team_wakaland) - - hero_tarantula = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) - hero_dr_weird = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) - hero_cap = Hero( - name="Captain North America", secret_name="Esteban Rogelios", age=93 - ) - - team_preventers.heroes.append(hero_tarantula) - team_preventers.heroes.append(hero_dr_weird) - team_preventers.heroes.append(hero_cap) - session.add(team_preventers) - session.commit() - session.refresh(hero_tarantula) - session.refresh(hero_dr_weird) - session.refresh(hero_cap) - print("Preventers new hero:", hero_tarantula) - print("Preventers new hero:", hero_dr_weird) - print("Preventers new hero:", hero_cap) - - -def select_heroes(): - with Session(engine) as session: - statement = select(Team).where(Team.name == "Preventers") - result = session.exec(statement) - team_preventers = result.one() - - print("Preventers heroes:", team_preventers.heroes) - - -def update_heroes(): - with Session(engine) as session: - hero_spider_boy = session.exec( - select(Hero).where(Hero.name == "Spider-Boy") - ).one() - - preventers_team = session.exec( - select(Team).where(Team.name == "Preventers") - ).one() - - print("Hero Spider-Boy:", hero_spider_boy) - print("Preventers Team:", preventers_team) - print("Preventers Team Heroes:", preventers_team.heroes) - - hero_spider_boy.team = None - - print("Spider-Boy without team:", hero_spider_boy) - - print("Preventers Team Heroes again:", preventers_team.heroes) - - session.add(hero_spider_boy) - session.commit() - print("After committing") - - session.refresh(hero_spider_boy) - print("Spider-Boy after commit:", hero_spider_boy) - - print("Preventers Team Heroes after commit:", preventers_team.heroes) - - -def main(): - create_db_and_tables() - create_heroes() - select_heroes() - update_heroes() - - -if __name__ == "__main__": - main() diff --git a/docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py b/docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py deleted file mode 100644 index 98e197002e..0000000000 --- a/docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py +++ /dev/null @@ -1,59 +0,0 @@ -from typing import List, Optional - -from sqlmodel import Field, Relationship, SQLModel, create_engine - - -class Weapon(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - - hero: "Hero" = Relationship(back_populates="weapon") - - -class Power(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - - hero_id: int = Field(foreign_key="hero.id") - hero: "Hero" = Relationship(back_populates="powers") - - -class Team(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - headquarters: str - - heroes: List["Hero"] = Relationship(back_populates="team") - - -class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - secret_name: str - age: Optional[int] = Field(default=None, index=True) - - team_id: Optional[int] = Field(default=None, foreign_key="team.id") - team: Optional[Team] = Relationship(back_populates="heroes") - - weapon_id: Optional[int] = Field(default=None, foreign_key="weapon.id") - weapon: Optional[Weapon] = Relationship(back_populates="hero") - - powers: List[Power] = Relationship(back_populates="hero") - - -sqlite_file_name = "database.db" -sqlite_url = f"sqlite:///{sqlite_file_name}" - -engine = create_engine(sqlite_url, echo=True) - - -def create_db_and_tables(): - SQLModel.metadata.create_all(engine) - - -def main(): - create_db_and_tables() - - -if __name__ == "__main__": - main() diff --git a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001.py b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001.py deleted file mode 100644 index 877aeb3c67..0000000000 --- a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial001.py +++ /dev/null @@ -1,110 +0,0 @@ -from typing import List, Optional - -from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select - - -class Team(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - headquarters: str - - heroes: List["Hero"] = Relationship(back_populates="team", cascade_delete=True) - - -class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - secret_name: str - age: Optional[int] = Field(default=None, index=True) - - team_id: Optional[int] = Field( - default=None, foreign_key="team.id", ondelete="CASCADE" - ) - team: Optional[Team] = Relationship(back_populates="heroes") - - -sqlite_file_name = "database.db" -sqlite_url = f"sqlite:///{sqlite_file_name}" - -engine = create_engine(sqlite_url, echo=True) - - -def create_db_and_tables(): - SQLModel.metadata.create_all(engine) - - -def create_heroes(): - with Session(engine) as session: - team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") - - hero_deadpond = Hero( - name="Deadpond", secret_name="Dive Wilson", team=team_z_force - ) - hero_rusty_man = Hero( - name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers - ) - 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) - session.commit() - - session.refresh(hero_deadpond) - session.refresh(hero_rusty_man) - 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 = team_preventers - session.add(hero_spider_boy) - session.commit() - session.refresh(hero_spider_boy) - print("Updated hero:", hero_spider_boy) - - hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) - hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") - team_wakaland = Team( - name="Wakaland", - headquarters="Wakaland Capital City", - heroes=[hero_black_lion, hero_sure_e], - ) - session.add(team_wakaland) - session.commit() - session.refresh(team_wakaland) - print("Team Wakaland:", team_wakaland) - - -def delete_team(): - with Session(engine) as session: - statement = select(Team).where(Team.name == "Wakaland") - team = session.exec(statement).one() - session.delete(team) - session.commit() - print("Deleted team:", team) - - -def select_deleted_heroes(): - with Session(engine) as session: - statement = select(Hero).where(Hero.name == "Black Lion") - result = session.exec(statement) - hero = result.first() - print("Black Lion not found:", hero) - - statement = select(Hero).where(Hero.name == "Princess Sure-E") - result = session.exec(statement) - hero = result.first() - print("Princess Sure-E not found:", hero) - - -def main(): - create_db_and_tables() - create_heroes() - delete_team() - select_deleted_heroes() - - -if __name__ == "__main__": - main() diff --git a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002.py b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002.py deleted file mode 100644 index ac97a20f28..0000000000 --- a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial002.py +++ /dev/null @@ -1,110 +0,0 @@ -from typing import List, Optional - -from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select - - -class Team(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - headquarters: str - - heroes: List["Hero"] = Relationship(back_populates="team") - - -class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - secret_name: str - age: Optional[int] = Field(default=None, index=True) - - team_id: Optional[int] = Field( - default=None, foreign_key="team.id", ondelete="SET NULL" - ) - team: Optional[Team] = Relationship(back_populates="heroes") - - -sqlite_file_name = "database.db" -sqlite_url = f"sqlite:///{sqlite_file_name}" - -engine = create_engine(sqlite_url, echo=True) - - -def create_db_and_tables(): - SQLModel.metadata.create_all(engine) - - -def create_heroes(): - with Session(engine) as session: - team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") - - hero_deadpond = Hero( - name="Deadpond", secret_name="Dive Wilson", team=team_z_force - ) - hero_rusty_man = Hero( - name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers - ) - 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) - session.commit() - - session.refresh(hero_deadpond) - session.refresh(hero_rusty_man) - 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 = team_preventers - session.add(hero_spider_boy) - session.commit() - session.refresh(hero_spider_boy) - print("Updated hero:", hero_spider_boy) - - hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) - hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") - team_wakaland = Team( - name="Wakaland", - headquarters="Wakaland Capital City", - heroes=[hero_black_lion, hero_sure_e], - ) - session.add(team_wakaland) - session.commit() - session.refresh(team_wakaland) - print("Team Wakaland:", team_wakaland) - - -def delete_team(): - with Session(engine) as session: - statement = select(Team).where(Team.name == "Wakaland") - team = session.exec(statement).one() - session.delete(team) - session.commit() - print("Deleted team:", team) - - -def select_deleted_heroes(): - with Session(engine) as session: - statement = select(Hero).where(Hero.name == "Black Lion") - result = session.exec(statement) - hero = result.first() - print("Black Lion has no team:", hero) - - statement = select(Hero).where(Hero.name == "Princess Sure-E") - result = session.exec(statement) - hero = result.first() - print("Princess Sure-E has no team:", hero) - - -def main(): - create_db_and_tables() - create_heroes() - delete_team() - select_deleted_heroes() - - -if __name__ == "__main__": - main() diff --git a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003.py b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003.py deleted file mode 100644 index 0424f75614..0000000000 --- a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial003.py +++ /dev/null @@ -1,112 +0,0 @@ -from typing import List, Optional - -from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select, text - - -class Team(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - headquarters: str - - heroes: List["Hero"] = Relationship(back_populates="team", passive_deletes="all") - - -class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - secret_name: str - age: Optional[int] = Field(default=None, index=True) - - team_id: Optional[int] = Field( - default=None, foreign_key="team.id", ondelete="SET NULL" - ) - team: Optional[Team] = Relationship(back_populates="heroes") - - -sqlite_file_name = "database.db" -sqlite_url = f"sqlite:///{sqlite_file_name}" - -engine = create_engine(sqlite_url, echo=True) - - -def create_db_and_tables(): - SQLModel.metadata.create_all(engine) - with engine.connect() as connection: - connection.execute(text("PRAGMA foreign_keys=ON")) # for SQLite only - - -def create_heroes(): - with Session(engine) as session: - team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") - - hero_deadpond = Hero( - name="Deadpond", secret_name="Dive Wilson", team=team_z_force - ) - hero_rusty_man = Hero( - name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers - ) - 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) - session.commit() - - session.refresh(hero_deadpond) - session.refresh(hero_rusty_man) - 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 = team_preventers - session.add(hero_spider_boy) - session.commit() - session.refresh(hero_spider_boy) - print("Updated hero:", hero_spider_boy) - - hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) - hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") - team_wakaland = Team( - name="Wakaland", - headquarters="Wakaland Capital City", - heroes=[hero_black_lion, hero_sure_e], - ) - session.add(team_wakaland) - session.commit() - session.refresh(team_wakaland) - print("Team Wakaland:", team_wakaland) - - -def delete_team(): - with Session(engine) as session: - statement = select(Team).where(Team.name == "Wakaland") - team = session.exec(statement).one() - session.delete(team) - session.commit() - print("Deleted team:", team) - - -def select_deleted_heroes(): - with Session(engine) as session: - statement = select(Hero).where(Hero.name == "Black Lion") - result = session.exec(statement) - hero = result.first() - print("Black Lion has no team:", hero) - - statement = select(Hero).where(Hero.name == "Princess Sure-E") - result = session.exec(statement) - hero = result.first() - print("Princess Sure-E has no team:", hero) - - -def main(): - create_db_and_tables() - create_heroes() - delete_team() - select_deleted_heroes() - - -if __name__ == "__main__": - main() diff --git a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004.py b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004.py deleted file mode 100644 index 00c5ac1765..0000000000 --- a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial004.py +++ /dev/null @@ -1,111 +0,0 @@ -from typing import List, Optional - -from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select, text - - -class Team(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - headquarters: str - - heroes: List["Hero"] = Relationship(back_populates="team", passive_deletes="all") - - -class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - secret_name: str - age: Optional[int] = Field(default=None, index=True) - - team_id: Optional[int] = Field( - default=None, foreign_key="team.id", ondelete="RESTRICT" - ) - team: Optional[Team] = Relationship(back_populates="heroes") - - -sqlite_file_name = "database.db" -sqlite_url = f"sqlite:///{sqlite_file_name}" - -engine = create_engine(sqlite_url, echo=True) - - -def create_db_and_tables(): - SQLModel.metadata.create_all(engine) - with engine.connect() as connection: - connection.execute(text("PRAGMA foreign_keys=ON")) # for SQLite only - - -def create_heroes(): - with Session(engine) as session: - team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") - - hero_deadpond = Hero( - name="Deadpond", secret_name="Dive Wilson", team=team_z_force - ) - hero_rusty_man = Hero( - name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers - ) - 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) - session.commit() - - session.refresh(hero_deadpond) - session.refresh(hero_rusty_man) - 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 = team_preventers - session.add(hero_spider_boy) - session.commit() - session.refresh(hero_spider_boy) - print("Updated hero:", hero_spider_boy) - - hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) - hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") - team_wakaland = Team( - name="Wakaland", - headquarters="Wakaland Capital City", - heroes=[hero_black_lion, hero_sure_e], - ) - session.add(team_wakaland) - session.commit() - session.refresh(team_wakaland) - print("Team Wakaland:", team_wakaland) - - -def delete_team(): - with Session(engine) as session: - statement = select(Team).where(Team.name == "Wakaland") - team = session.exec(statement).one() - session.delete(team) - session.commit() - print("Deleted team:", team) - - -def select_deleted_heroes(): - with Session(engine) as session: - statement = select(Hero).where(Hero.name == "Black Lion") - result = session.exec(statement) - hero = result.first() - print("Black Lion has no team:", hero) - - statement = select(Hero).where(Hero.name == "Princess Sure-E") - result = session.exec(statement) - hero = result.first() - print("Princess Sure-E has no team:", hero) - - -def main(): - create_db_and_tables() - create_heroes() - delete_team() - - -if __name__ == "__main__": - main() diff --git a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005.py b/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005.py deleted file mode 100644 index b46a7781bc..0000000000 --- a/docs_src/tutorial/relationship_attributes/cascade_delete_relationships/tutorial005.py +++ /dev/null @@ -1,124 +0,0 @@ -from typing import List, Optional - -from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select, text - - -class Team(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - headquarters: str - - heroes: List["Hero"] = Relationship(back_populates="team", passive_deletes="all") - - -class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - secret_name: str - age: Optional[int] = Field(default=None, index=True) - - team_id: Optional[int] = Field( - default=None, foreign_key="team.id", ondelete="RESTRICT" - ) - team: Optional[Team] = Relationship(back_populates="heroes") - - -sqlite_file_name = "database.db" -sqlite_url = f"sqlite:///{sqlite_file_name}" - -engine = create_engine(sqlite_url, echo=True) - - -def create_db_and_tables(): - SQLModel.metadata.create_all(engine) - with engine.connect() as connection: - connection.execute(text("PRAGMA foreign_keys=ON")) # for SQLite only - - -def create_heroes(): - with Session(engine) as session: - team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") - - hero_deadpond = Hero( - name="Deadpond", secret_name="Dive Wilson", team=team_z_force - ) - hero_rusty_man = Hero( - name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers - ) - 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) - session.commit() - - session.refresh(hero_deadpond) - session.refresh(hero_rusty_man) - 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 = team_preventers - session.add(hero_spider_boy) - session.commit() - session.refresh(hero_spider_boy) - print("Updated hero:", hero_spider_boy) - - hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) - hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") - team_wakaland = Team( - name="Wakaland", - headquarters="Wakaland Capital City", - heroes=[hero_black_lion, hero_sure_e], - ) - session.add(team_wakaland) - session.commit() - session.refresh(team_wakaland) - print("Team Wakaland:", team_wakaland) - - -def remove_team_heroes(): - with Session(engine) as session: - statement = select(Team).where(Team.name == "Wakaland") - team = session.exec(statement).one() - team.heroes.clear() - session.add(team) - session.commit() - session.refresh(team) - print("Team with removed heroes:", team) - - -def delete_team(): - with Session(engine) as session: - statement = select(Team).where(Team.name == "Wakaland") - team = session.exec(statement).one() - session.delete(team) - session.commit() - print("Deleted team:", team) - - -def select_deleted_heroes(): - with Session(engine) as session: - statement = select(Hero).where(Hero.name == "Black Lion") - result = session.exec(statement) - hero = result.first() - print("Black Lion has no team:", hero) - - statement = select(Hero).where(Hero.name == "Princess Sure-E") - result = session.exec(statement) - hero = result.first() - print("Princess Sure-E has no team:", hero) - - -def main(): - create_db_and_tables() - create_heroes() - remove_team_heroes() - delete_team() - select_deleted_heroes() - - -if __name__ == "__main__": - main() diff --git a/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py b/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py deleted file mode 100644 index 73c68cf965..0000000000 --- a/docs_src/tutorial/relationship_attributes/create_and_update_relationships/tutorial001.py +++ /dev/null @@ -1,102 +0,0 @@ -from typing import List, Optional - -from sqlmodel import Field, Relationship, Session, SQLModel, create_engine - - -class Team(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - headquarters: str - - heroes: List["Hero"] = Relationship(back_populates="team") - - -class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - secret_name: str - age: Optional[int] = Field(default=None, index=True) - - team_id: Optional[int] = Field(default=None, foreign_key="team.id") - team: Optional[Team] = Relationship(back_populates="heroes") - - -sqlite_file_name = "database.db" -sqlite_url = f"sqlite:///{sqlite_file_name}" - -engine = create_engine(sqlite_url, echo=True) - - -def create_db_and_tables(): - SQLModel.metadata.create_all(engine) - - -def create_heroes(): - with Session(engine) as session: - team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") - - hero_deadpond = Hero( - name="Deadpond", secret_name="Dive Wilson", team=team_z_force - ) - hero_rusty_man = Hero( - name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers - ) - 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) - session.commit() - - session.refresh(hero_deadpond) - session.refresh(hero_rusty_man) - 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 = team_preventers - session.add(hero_spider_boy) - session.commit() - session.refresh(hero_spider_boy) - print("Updated hero:", hero_spider_boy) - - hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) - hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") - team_wakaland = Team( - name="Wakaland", - headquarters="Wakaland Capital City", - heroes=[hero_black_lion, hero_sure_e], - ) - session.add(team_wakaland) - session.commit() - session.refresh(team_wakaland) - print("Team Wakaland:", team_wakaland) - - hero_tarantula = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) - hero_dr_weird = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) - hero_cap = Hero( - name="Captain North America", secret_name="Esteban Rogelios", age=93 - ) - - team_preventers.heroes.append(hero_tarantula) - team_preventers.heroes.append(hero_dr_weird) - team_preventers.heroes.append(hero_cap) - session.add(team_preventers) - session.commit() - session.refresh(hero_tarantula) - session.refresh(hero_dr_weird) - session.refresh(hero_cap) - print("Preventers new hero:", hero_tarantula) - print("Preventers new hero:", hero_dr_weird) - print("Preventers new hero:", hero_cap) - - -def main(): - create_db_and_tables() - create_heroes() - - -if __name__ == "__main__": - main() diff --git a/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py b/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py deleted file mode 100644 index af218b9567..0000000000 --- a/docs_src/tutorial/relationship_attributes/define_relationship_attributes/tutorial001.py +++ /dev/null @@ -1,70 +0,0 @@ -from typing import List, Optional - -from sqlmodel import Field, Relationship, Session, SQLModel, create_engine - - -class Team(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - headquarters: str - - heroes: List["Hero"] = Relationship(back_populates="team") - - -class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - secret_name: str - age: Optional[int] = Field(default=None, index=True) - - team_id: Optional[int] = Field(default=None, foreign_key="team.id") - team: Optional[Team] = Relationship(back_populates="heroes") - - -sqlite_file_name = "database.db" -sqlite_url = f"sqlite:///{sqlite_file_name}" - -engine = create_engine(sqlite_url, echo=True) - - -def create_db_and_tables(): - SQLModel.metadata.create_all(engine) - - -def create_heroes(): - with Session(engine) as session: - team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") - - hero_deadpond = Hero( - name="Deadpond", secret_name="Dive Wilson", team=team_z_force - ) - hero_rusty_man = Hero( - name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers - ) - 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) - session.commit() - - session.refresh(hero_deadpond) - session.refresh(hero_rusty_man) - 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 = team_preventers - session.add(hero_spider_boy) - session.commit() - - -def main(): - create_db_and_tables() - create_heroes() - - -if __name__ == "__main__": - main() diff --git a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py deleted file mode 100644 index b8bbe70a4c..0000000000 --- a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial001.py +++ /dev/null @@ -1,117 +0,0 @@ -from typing import List, Optional - -from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select - - -class Team(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - headquarters: str - - heroes: List["Hero"] = Relationship(back_populates="team") - - -class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - secret_name: str - age: Optional[int] = Field(default=None, index=True) - - team_id: Optional[int] = Field(default=None, foreign_key="team.id") - team: Optional[Team] = Relationship(back_populates="heroes") - - -sqlite_file_name = "database.db" -sqlite_url = f"sqlite:///{sqlite_file_name}" - -engine = create_engine(sqlite_url, echo=True) - - -def create_db_and_tables(): - SQLModel.metadata.create_all(engine) - - -def create_heroes(): - with Session(engine) as session: - team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") - - hero_deadpond = Hero( - name="Deadpond", secret_name="Dive Wilson", team=team_z_force - ) - hero_rusty_man = Hero( - name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers - ) - 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) - session.commit() - - session.refresh(hero_deadpond) - session.refresh(hero_rusty_man) - 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 = team_preventers - session.add(hero_spider_boy) - session.commit() - session.refresh(hero_spider_boy) - print("Updated hero:", hero_spider_boy) - - hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) - hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") - team_wakaland = Team( - name="Wakaland", - headquarters="Wakaland Capital City", - heroes=[hero_black_lion, hero_sure_e], - ) - session.add(team_wakaland) - session.commit() - session.refresh(team_wakaland) - print("Team Wakaland:", team_wakaland) - - hero_tarantula = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) - hero_dr_weird = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) - hero_cap = Hero( - name="Captain North America", secret_name="Esteban Rogelios", age=93 - ) - - team_preventers.heroes.append(hero_tarantula) - team_preventers.heroes.append(hero_dr_weird) - team_preventers.heroes.append(hero_cap) - session.add(team_preventers) - session.commit() - session.refresh(hero_tarantula) - session.refresh(hero_dr_weird) - session.refresh(hero_cap) - print("Preventers new hero:", hero_tarantula) - print("Preventers new hero:", hero_dr_weird) - print("Preventers new hero:", hero_cap) - - -def select_heroes(): - with Session(engine) as session: - statement = select(Hero).where(Hero.name == "Spider-Boy") - result = session.exec(statement) - hero_spider_boy = result.one() - - statement = select(Team).where(Team.id == hero_spider_boy.team_id) - result = session.exec(statement) - team = result.first() - print("Spider-Boy's team:", team) - - print("Spider-Boy's team again:", hero_spider_boy.team) - - -def main(): - create_db_and_tables() - create_heroes() - select_heroes() - - -if __name__ == "__main__": - main() diff --git a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py b/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py deleted file mode 100644 index 30fa840878..0000000000 --- a/docs_src/tutorial/relationship_attributes/read_relationships/tutorial002.py +++ /dev/null @@ -1,127 +0,0 @@ -from typing import List, Optional - -from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select - - -class Team(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - headquarters: str - - heroes: List["Hero"] = Relationship(back_populates="team") - - -class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - secret_name: str - age: Optional[int] = Field(default=None, index=True) - - team_id: Optional[int] = Field(default=None, foreign_key="team.id") - team: Optional[Team] = Relationship(back_populates="heroes") - - -sqlite_file_name = "database.db" -sqlite_url = f"sqlite:///{sqlite_file_name}" - -engine = create_engine(sqlite_url, echo=True) - - -def create_db_and_tables(): - SQLModel.metadata.create_all(engine) - - -def create_heroes(): - with Session(engine) as session: - team_preventers = Team(name="Preventers", headquarters="Sharp Tower") - team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar") - - hero_deadpond = Hero( - name="Deadpond", secret_name="Dive Wilson", team=team_z_force - ) - hero_rusty_man = Hero( - name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers - ) - 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) - session.commit() - - session.refresh(hero_deadpond) - session.refresh(hero_rusty_man) - 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 = team_preventers - session.add(hero_spider_boy) - session.commit() - session.refresh(hero_spider_boy) - print("Updated hero:", hero_spider_boy) - - hero_black_lion = Hero(name="Black Lion", secret_name="Trevor Challa", age=35) - hero_sure_e = Hero(name="Princess Sure-E", secret_name="Sure-E") - team_wakaland = Team( - name="Wakaland", - headquarters="Wakaland Capital City", - heroes=[hero_black_lion, hero_sure_e], - ) - session.add(team_wakaland) - session.commit() - session.refresh(team_wakaland) - print("Team Wakaland:", team_wakaland) - - hero_tarantula = Hero(name="Tarantula", secret_name="Natalia Roman-on", age=32) - hero_dr_weird = Hero(name="Dr. Weird", secret_name="Steve Weird", age=36) - hero_cap = Hero( - name="Captain North America", secret_name="Esteban Rogelios", age=93 - ) - - team_preventers.heroes.append(hero_tarantula) - team_preventers.heroes.append(hero_dr_weird) - team_preventers.heroes.append(hero_cap) - session.add(team_preventers) - session.commit() - session.refresh(hero_tarantula) - session.refresh(hero_dr_weird) - session.refresh(hero_cap) - print("Preventers new hero:", hero_tarantula) - print("Preventers new hero:", hero_dr_weird) - print("Preventers new hero:", hero_cap) - - -def select_heroes(): - with Session(engine) as session: - statement = select(Team).where(Team.name == "Preventers") - result = session.exec(statement) - team_preventers = result.one() - - print("Preventers heroes:", team_preventers.heroes) - - -def update_heroes(): - with Session(engine) as session: - statement = select(Hero).where(Hero.name == "Spider-Boy") - result = session.exec(statement) - hero_spider_boy = result.one() - - hero_spider_boy.team = None - session.add(hero_spider_boy) - session.commit() - - session.refresh(hero_spider_boy) - print("Spider-Boy without team:", hero_spider_boy) - - -def main(): - create_db_and_tables() - create_heroes() - select_heroes() - update_heroes() - - -if __name__ == "__main__": - main() diff --git a/docs_src/tutorial/select/tutorial001.py b/docs_src/tutorial/select/tutorial001_py39.py similarity index 100% rename from docs_src/tutorial/select/tutorial001.py rename to docs_src/tutorial/select/tutorial001_py39.py diff --git a/docs_src/tutorial/select/tutorial002.py b/docs_src/tutorial/select/tutorial002_py39.py similarity index 100% rename from docs_src/tutorial/select/tutorial002.py rename to docs_src/tutorial/select/tutorial002_py39.py diff --git a/docs_src/tutorial/select/tutorial003.py b/docs_src/tutorial/select/tutorial003_py39.py similarity index 100% rename from docs_src/tutorial/select/tutorial003.py rename to docs_src/tutorial/select/tutorial003_py39.py diff --git a/docs_src/tutorial/select/tutorial004.py b/docs_src/tutorial/select/tutorial004_py39.py similarity index 100% rename from docs_src/tutorial/select/tutorial004.py rename to docs_src/tutorial/select/tutorial004_py39.py diff --git a/docs_src/tutorial/update/tutorial001.py b/docs_src/tutorial/update/tutorial001_py39.py similarity index 100% rename from docs_src/tutorial/update/tutorial001.py rename to docs_src/tutorial/update/tutorial001_py39.py diff --git a/docs_src/tutorial/update/tutorial002.py b/docs_src/tutorial/update/tutorial002_py39.py similarity index 100% rename from docs_src/tutorial/update/tutorial002.py rename to docs_src/tutorial/update/tutorial002_py39.py diff --git a/docs_src/tutorial/update/tutorial003.py b/docs_src/tutorial/update/tutorial003_py39.py similarity index 100% rename from docs_src/tutorial/update/tutorial003.py rename to docs_src/tutorial/update/tutorial003_py39.py diff --git a/docs_src/tutorial/update/tutorial004.py b/docs_src/tutorial/update/tutorial004_py39.py similarity index 100% rename from docs_src/tutorial/update/tutorial004.py rename to docs_src/tutorial/update/tutorial004_py39.py diff --git a/docs_src/tutorial/where/tutorial001.py b/docs_src/tutorial/where/tutorial001_py39.py similarity index 100% rename from docs_src/tutorial/where/tutorial001.py rename to docs_src/tutorial/where/tutorial001_py39.py diff --git a/docs_src/tutorial/where/tutorial002.py b/docs_src/tutorial/where/tutorial002_py39.py similarity index 100% rename from docs_src/tutorial/where/tutorial002.py rename to docs_src/tutorial/where/tutorial002_py39.py diff --git a/docs_src/tutorial/where/tutorial003.py b/docs_src/tutorial/where/tutorial003_py39.py similarity index 100% rename from docs_src/tutorial/where/tutorial003.py rename to docs_src/tutorial/where/tutorial003_py39.py diff --git a/docs_src/tutorial/where/tutorial004.py b/docs_src/tutorial/where/tutorial004_py39.py similarity index 100% rename from docs_src/tutorial/where/tutorial004.py rename to docs_src/tutorial/where/tutorial004_py39.py diff --git a/docs_src/tutorial/where/tutorial005.py b/docs_src/tutorial/where/tutorial005_py39.py similarity index 100% rename from docs_src/tutorial/where/tutorial005.py rename to docs_src/tutorial/where/tutorial005_py39.py diff --git a/docs_src/tutorial/where/tutorial006.py b/docs_src/tutorial/where/tutorial006_py39.py similarity index 100% rename from docs_src/tutorial/where/tutorial006.py rename to docs_src/tutorial/where/tutorial006_py39.py diff --git a/docs_src/tutorial/where/tutorial007.py b/docs_src/tutorial/where/tutorial007_py39.py similarity index 100% rename from docs_src/tutorial/where/tutorial007.py rename to docs_src/tutorial/where/tutorial007_py39.py diff --git a/docs_src/tutorial/where/tutorial008.py b/docs_src/tutorial/where/tutorial008_py39.py similarity index 100% rename from docs_src/tutorial/where/tutorial008.py rename to docs_src/tutorial/where/tutorial008_py39.py diff --git a/docs_src/tutorial/where/tutorial009.py b/docs_src/tutorial/where/tutorial009_py39.py similarity index 100% rename from docs_src/tutorial/where/tutorial009.py rename to docs_src/tutorial/where/tutorial009_py39.py diff --git a/docs_src/tutorial/where/tutorial010.py b/docs_src/tutorial/where/tutorial010_py39.py similarity index 100% rename from docs_src/tutorial/where/tutorial010.py rename to docs_src/tutorial/where/tutorial010_py39.py diff --git a/docs_src/tutorial/where/tutorial011.py b/docs_src/tutorial/where/tutorial011_py39.py similarity index 100% rename from docs_src/tutorial/where/tutorial011.py rename to docs_src/tutorial/where/tutorial011_py39.py diff --git a/requirements-tests.txt b/requirements-tests.txt index 936791db50..378eaca669 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -2,15 +2,11 @@ -r requirements-docs-tests.txt pytest >=7.0.1,<9.0.0 coverage[toml] >=6.2,<8.0 -# Remove when support for Python 3.8 is dropped -mypy ==1.14.1; python_version < "3.9" -mypy ==1.19.1; python_version >= "3.9" +mypy ==1.19.1 ruff ==0.14.10 # For FastAPI tests fastapi >=0.103.2,<0.126.0 httpx ==0.28.1 dirty-equals ==0.9.0 jinja2 ==3.1.6 -# Remove when support for Python 3.8 is dropped -typing-extensions ==4.13.2; python_version < "3.9" -typing-extensions ==4.15.0; python_version >= "3.9" +typing-extensions ==4.15.0 diff --git a/tests/conftest.py b/tests/conftest.py index 98a4d2b7e6..d8da629db0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -31,7 +31,7 @@ def clear_sqlmodel() -> Any: def cov_tmp_path(tmp_path: Path) -> Generator[Path, None, None]: yield tmp_path for coverage_path in tmp_path.glob(".coverage*"): - coverage_destiny_path = top_level_path / coverage_path.name + coverage_destiny_path = top_level_path / "coverage" / coverage_path.name shutil.copy(coverage_path, coverage_destiny_path) @@ -89,7 +89,6 @@ def print_mock_fixture() -> Generator[PrintMock, None, None]: needs_pydanticv2 = pytest.mark.skipif(not IS_PYDANTIC_V2, reason="requires Pydantic v2") needs_pydanticv1 = pytest.mark.skipif(IS_PYDANTIC_V2, reason="requires Pydantic v1") -needs_py39 = pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python3.9+") needs_py310 = pytest.mark.skipif( sys.version_info < (3, 10), reason="requires python3.10+" ) diff --git a/tests/test_advanced/test_decimal/test_tutorial001.py b/tests/test_advanced/test_decimal/test_tutorial001.py index dad8f24c4c..2e839e0e26 100644 --- a/tests/test_advanced/test_decimal/test_tutorial001.py +++ b/tests/test_advanced/test_decimal/test_tutorial001.py @@ -11,7 +11,7 @@ @pytest.fixture( name="mod", params=[ - "tutorial001", + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), ], ) diff --git a/tests/test_advanced/test_uuid/test_tutorial001.py b/tests/test_advanced/test_uuid/test_tutorial001.py index ab4f3387dc..e453ef00e2 100644 --- a/tests/test_advanced/test_uuid/test_tutorial001.py +++ b/tests/test_advanced/test_uuid/test_tutorial001.py @@ -11,7 +11,7 @@ @pytest.fixture( name="mod", params=[ - "tutorial001", + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), ], ) diff --git a/tests/test_advanced/test_uuid/test_tutorial002.py b/tests/test_advanced/test_uuid/test_tutorial002.py index b1ff5ba3fd..e171c1e0ea 100644 --- a/tests/test_advanced/test_uuid/test_tutorial002.py +++ b/tests/test_advanced/test_uuid/test_tutorial002.py @@ -11,7 +11,7 @@ @pytest.fixture( name="mod", params=[ - "tutorial002", + pytest.param("tutorial002_py39"), pytest.param("tutorial002_py310", marks=needs_py310), ], ) diff --git a/tests/test_select_gen.py b/tests/test_select_gen.py index 664cebf2a7..94ed340e94 100644 --- a/tests/test_select_gen.py +++ b/tests/test_select_gen.py @@ -3,12 +3,9 @@ import sys from pathlib import Path -from .conftest import needs_py39 - root_path = Path(__file__).parent.parent -@needs_py39 def test_select_gen() -> None: env = os.environ.copy() env["CHECK_JINJA"] = "1" diff --git a/tests/test_tutorial/test_automatic_id_none_refresh/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_automatic_id_none_refresh/test_tutorial001_tutorial002.py index 7233e40be8..b2c452f907 100644 --- a/tests/test_tutorial/test_automatic_id_none_refresh/test_tutorial001_tutorial002.py +++ b/tests/test_tutorial/test_automatic_id_none_refresh/test_tutorial001_tutorial002.py @@ -138,8 +138,8 @@ def check_calls(calls: List[List[Union[str, Dict[str, Any]]]]) -> None: @pytest.fixture( name="module", params=[ - "tutorial001", - "tutorial002", + "tutorial001_py39", + "tutorial002_py39", pytest.param("tutorial001_py310", marks=needs_py310), pytest.param("tutorial002_py310", marks=needs_py310), ], diff --git a/tests/test_tutorial/test_code_structure/test_tutorial001.py b/tests/test_tutorial/test_code_structure/test_tutorial001.py index 99ae5c00f0..aa832f6223 100644 --- a/tests/test_tutorial/test_code_structure/test_tutorial001.py +++ b/tests/test_tutorial/test_code_structure/test_tutorial001.py @@ -5,7 +5,7 @@ import pytest from sqlmodel import create_engine -from tests.conftest import PrintMock, needs_py39, needs_py310 +from tests.conftest import PrintMock, needs_py310 expected_calls = [ [ @@ -34,8 +34,7 @@ class Modules: @pytest.fixture( name="modules", params=[ - "tutorial001", - pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_code_structure/test_tutorial002.py b/tests/test_tutorial/test_code_structure/test_tutorial002.py index f1d4043e85..990d3ecaaa 100644 --- a/tests/test_tutorial/test_code_structure/test_tutorial002.py +++ b/tests/test_tutorial/test_code_structure/test_tutorial002.py @@ -5,7 +5,7 @@ import pytest from sqlmodel import create_engine -from ...conftest import PrintMock, needs_py39, needs_py310 +from ...conftest import PrintMock, needs_py310 expected_calls = [ [ @@ -34,8 +34,7 @@ class Modules: @pytest.fixture( name="modules", params=[ - "tutorial002", - pytest.param("tutorial002_py39", marks=needs_py39), + pytest.param("tutorial002_py39"), pytest.param("tutorial002_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_connect/test_create_connected_tables/test_tutorial001.py b/tests/test_tutorial/test_connect/test_create_connected_tables/test_tutorial001.py index 49d4acfa00..66e57debeb 100644 --- a/tests/test_tutorial/test_connect/test_create_connected_tables/test_tutorial001.py +++ b/tests/test_tutorial/test_connect/test_create_connected_tables/test_tutorial001.py @@ -12,7 +12,7 @@ @pytest.fixture( name="module", params=[ - "tutorial001", + "tutorial001_py39", pytest.param("tutorial001_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_connect/test_delete/test_tutorial001.py b/tests/test_tutorial/test_connect/test_delete/test_tutorial001.py index cc25dd42e3..7e20767f59 100644 --- a/tests/test_tutorial/test_connect/test_delete/test_tutorial001.py +++ b/tests/test_tutorial/test_connect/test_delete/test_tutorial001.py @@ -63,7 +63,7 @@ @pytest.fixture( name="module", params=[ - "tutorial001", + "tutorial001_py39", pytest.param("tutorial001_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_connect/test_insert/test_tutorial001.py b/tests/test_tutorial/test_connect/test_insert/test_tutorial001.py index 6c4ec3ab1c..c7363a0731 100644 --- a/tests/test_tutorial/test_connect/test_insert/test_tutorial001.py +++ b/tests/test_tutorial/test_connect/test_insert/test_tutorial001.py @@ -43,7 +43,7 @@ @pytest.fixture( name="module", params=[ - "tutorial001", + "tutorial001_py39", pytest.param("tutorial001_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_connect/test_select/test_tutorial001_tutorial002.py index 6553bc1ae6..4b141f11d1 100644 --- a/tests/test_tutorial/test_connect/test_select/test_tutorial001_tutorial002.py +++ b/tests/test_tutorial/test_connect/test_select/test_tutorial001_tutorial002.py @@ -77,7 +77,7 @@ def get_module(request: pytest.FixtureRequest) -> ModuleType: @pytest.mark.parametrize( "module", [ - "tutorial001", + "tutorial001_py39", pytest.param("tutorial001_py310", marks=needs_py310), ], indirect=True, @@ -90,7 +90,7 @@ def test_tutorial001(print_mock: PrintMock, module: ModuleType): @pytest.mark.parametrize( "module", [ - "tutorial002", + "tutorial002_py39", pytest.param("tutorial002_py310", marks=needs_py310), ], indirect=True, diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial003.py b/tests/test_tutorial/test_connect/test_select/test_tutorial003.py index 6036b672a3..3ded56065c 100644 --- a/tests/test_tutorial/test_connect/test_select/test_tutorial003.py +++ b/tests/test_tutorial/test_connect/test_select/test_tutorial003.py @@ -79,7 +79,7 @@ @pytest.fixture( name="module", params=[ - "tutorial003", + "tutorial003_py39", pytest.param("tutorial003_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial004.py b/tests/test_tutorial/test_connect/test_select/test_tutorial004.py index c9fa695eda..7e8240bb09 100644 --- a/tests/test_tutorial/test_connect/test_select/test_tutorial004.py +++ b/tests/test_tutorial/test_connect/test_select/test_tutorial004.py @@ -53,7 +53,7 @@ @pytest.fixture( name="module", params=[ - "tutorial004", + "tutorial004_py39", pytest.param("tutorial004_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_connect/test_select/test_tutorial005.py b/tests/test_tutorial/test_connect/test_select/test_tutorial005.py index 12d98b7e66..004229a29b 100644 --- a/tests/test_tutorial/test_connect/test_select/test_tutorial005.py +++ b/tests/test_tutorial/test_connect/test_select/test_tutorial005.py @@ -55,7 +55,7 @@ @pytest.fixture( name="module", params=[ - "tutorial005", + "tutorial005_py39", pytest.param("tutorial005_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_connect/test_update/test_tutorial001.py b/tests/test_tutorial/test_connect/test_update/test_tutorial001.py index aa94c91216..7120dce7ed 100644 --- a/tests/test_tutorial/test_connect/test_update/test_tutorial001.py +++ b/tests/test_tutorial/test_connect/test_update/test_tutorial001.py @@ -53,7 +53,7 @@ @pytest.fixture( name="module", params=[ - "tutorial001", + "tutorial001_py39", pytest.param("tutorial001_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_create_db_and_table/test_tutorial001.py b/tests/test_tutorial/test_create_db_and_table/test_tutorial001.py index 4aeeb64b6c..3e054bf3eb 100644 --- a/tests/test_tutorial/test_create_db_and_table/test_tutorial001.py +++ b/tests/test_tutorial/test_create_db_and_table/test_tutorial001.py @@ -8,7 +8,7 @@ @pytest.mark.parametrize( "module_name", [ - "tutorial001", + "tutorial001_py39", pytest.param("tutorial001_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_create_db_and_table/test_tutorial002.py b/tests/test_tutorial/test_create_db_and_table/test_tutorial002.py index 0f1ad85fe9..f92bfee71d 100644 --- a/tests/test_tutorial/test_create_db_and_table/test_tutorial002.py +++ b/tests/test_tutorial/test_create_db_and_table/test_tutorial002.py @@ -12,7 +12,7 @@ @pytest.fixture( name="module", params=[ - "tutorial002", + "tutorial002_py39", pytest.param("tutorial002_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_create_db_and_table/test_tutorial003.py b/tests/test_tutorial/test_create_db_and_table/test_tutorial003.py index 446feae71a..e11e0a0472 100644 --- a/tests/test_tutorial/test_create_db_and_table/test_tutorial003.py +++ b/tests/test_tutorial/test_create_db_and_table/test_tutorial003.py @@ -12,7 +12,7 @@ @pytest.fixture( name="module", params=[ - "tutorial003", + "tutorial003_py39", pytest.param("tutorial003_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_delete/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_delete/test_tutorial001_tutorial002.py index 3d755ae027..5e6c354b3e 100644 --- a/tests/test_tutorial/test_delete/test_tutorial001_tutorial002.py +++ b/tests/test_tutorial/test_delete/test_tutorial001_tutorial002.py @@ -71,7 +71,7 @@ def get_module(request: pytest.FixtureRequest) -> ModuleType: @pytest.mark.parametrize( "module", [ - "tutorial001", + "tutorial001_py39", pytest.param("tutorial001_py310", marks=needs_py310), ], indirect=True, @@ -84,7 +84,7 @@ def test_tutorial001(print_mock: PrintMock, module: ModuleType): @pytest.mark.parametrize( "module", [ - "tutorial002", + "tutorial002_py39", pytest.param("tutorial002_py310", marks=needs_py310), ], indirect=True, diff --git a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests001.py b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests001.py index 95be986fc9..71fc72318f 100644 --- a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests001.py +++ b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests001.py @@ -5,7 +5,7 @@ import pytest -from tests.conftest import needs_py39, needs_py310 +from tests.conftest import needs_py310 @dataclass @@ -17,8 +17,7 @@ class Modules: @pytest.fixture( name="modules_path", params=[ - "tutorial001", - pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests002.py b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests002.py index 1f360c6f12..73bf080ec7 100644 --- a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests002.py +++ b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests002.py @@ -5,7 +5,7 @@ import pytest -from tests.conftest import needs_py39, needs_py310 +from tests.conftest import needs_py310 @dataclass @@ -17,8 +17,7 @@ class Modules: @pytest.fixture( name="modules_path", params=[ - "tutorial001", - pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), ], ) @@ -35,7 +34,7 @@ def load_modules(clear_sqlmodel, modules_path: str) -> Modules: app_mod = sys.modules[app_mod_path] importlib.reload(app_mod) else: - app_mod = importlib.import_module(app_mod_path) + app_mod = importlib.import_module(app_mod_path) # pragma: no cover test_mod = importlib.import_module(f"{modules_path}.test_main_002") return Modules(app=app_mod, test=test_mod) diff --git a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests003.py b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests003.py index 4911173b62..41c4d57301 100644 --- a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests003.py +++ b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests003.py @@ -5,7 +5,7 @@ import pytest -from tests.conftest import needs_py39, needs_py310 +from tests.conftest import needs_py310 @dataclass @@ -17,8 +17,7 @@ class Modules: @pytest.fixture( name="modules_path", params=[ - "tutorial001", - pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), ], ) @@ -35,7 +34,7 @@ def load_modules(clear_sqlmodel, modules_path: str) -> Modules: app_mod = sys.modules[app_mod_path] importlib.reload(app_mod) else: - app_mod = importlib.import_module(app_mod_path) + app_mod = importlib.import_module(app_mod_path) # pragma: no cover test_mod = importlib.import_module(f"{modules_path}.test_main_003") return Modules(app=app_mod, test=test_mod) diff --git a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests004.py b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests004.py index b3ca441d24..ba3a61ad1f 100644 --- a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests004.py +++ b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests004.py @@ -5,7 +5,7 @@ import pytest -from tests.conftest import needs_py39, needs_py310 +from tests.conftest import needs_py310 @dataclass @@ -17,8 +17,7 @@ class Modules: @pytest.fixture( name="modules_path", params=[ - "tutorial001", - pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), ], ) @@ -35,7 +34,7 @@ def load_modules(clear_sqlmodel, modules_path: str) -> Modules: app_mod = sys.modules[app_mod_path] importlib.reload(app_mod) else: - app_mod = importlib.import_module(app_mod_path) + app_mod = importlib.import_module(app_mod_path) # pragma: no cover test_mod = importlib.import_module(f"{modules_path}.test_main_004") return Modules(app=app_mod, test=test_mod) diff --git a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests005.py b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests005.py index cc550c4008..5d704ffb57 100644 --- a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests005.py +++ b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests005.py @@ -3,9 +3,11 @@ import pytest from sqlmodel import Session -from docs_src.tutorial.fastapi.app_testing.tutorial001 import main as app_mod -from docs_src.tutorial.fastapi.app_testing.tutorial001 import test_main_005 as test_mod -from docs_src.tutorial.fastapi.app_testing.tutorial001.test_main_005 import ( +from docs_src.tutorial.fastapi.app_testing.tutorial001_py39 import main as app_mod +from docs_src.tutorial.fastapi.app_testing.tutorial001_py39 import ( + test_main_005 as test_mod, +) +from docs_src.tutorial.fastapi.app_testing.tutorial001_py39.test_main_005 import ( session_fixture, ) diff --git a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests006.py b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests006.py index 67c9ac6ad4..1d474a7ec8 100644 --- a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests006.py +++ b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests006.py @@ -4,9 +4,11 @@ from fastapi.testclient import TestClient from sqlmodel import Session -from docs_src.tutorial.fastapi.app_testing.tutorial001 import main as app_mod -from docs_src.tutorial.fastapi.app_testing.tutorial001 import test_main_006 as test_mod -from docs_src.tutorial.fastapi.app_testing.tutorial001.test_main_006 import ( +from docs_src.tutorial.fastapi.app_testing.tutorial001_py39 import main as app_mod +from docs_src.tutorial.fastapi.app_testing.tutorial001_py39 import ( + test_main_006 as test_mod, +) +from docs_src.tutorial.fastapi.app_testing.tutorial001_py39.test_main_006 import ( client_fixture, session_fixture, ) diff --git a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests_main.py b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests_main.py index 28959f79d8..c6d942ea3f 100644 --- a/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests_main.py +++ b/tests/test_tutorial/test_fastapi/test_app_testing/test_tutorial001_tests_main.py @@ -5,14 +5,13 @@ import pytest -from ....conftest import needs_py39, needs_py310 +from ....conftest import needs_py310 @pytest.fixture( name="module", params=[ - "tutorial001", - pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py index 1480a06f54..ffbf7587f1 100644 --- a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py @@ -7,14 +7,13 @@ from sqlmodel import create_engine from sqlmodel.pool import StaticPool -from tests.conftest import needs_py39, needs_py310 +from tests.conftest import needs_py310 @pytest.fixture( name="module", params=[ - "tutorial001", - pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py index dc3db8b5b0..cbc722c5c2 100644 --- a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py @@ -7,14 +7,13 @@ from sqlmodel import create_engine from sqlmodel.pool import StaticPool -from tests.conftest import needs_py39, needs_py310 +from tests.conftest import needs_py310 @pytest.fixture( name="module", params=[ - "tutorial001", - pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py index 27629c891d..d70dce32e6 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py @@ -9,14 +9,13 @@ from sqlmodel import create_engine from sqlmodel.pool import StaticPool -from tests.conftest import needs_py39, needs_py310 +from tests.conftest import needs_py310 @pytest.fixture( name="module", params=[ - "tutorial001", - pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py index c840bde253..8d7c8064e7 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py @@ -9,14 +9,13 @@ from sqlmodel import create_engine from sqlmodel.pool import StaticPool -from tests.conftest import needs_py39, needs_py310 +from tests.conftest import needs_py310 @pytest.fixture( name="module", params=[ - "tutorial002", - pytest.param("tutorial002_py39", marks=needs_py39), + pytest.param("tutorial002_py39"), pytest.param("tutorial002_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py index affea513dd..7ebbecd8de 100644 --- a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py @@ -7,14 +7,13 @@ from sqlmodel import create_engine from sqlmodel.pool import StaticPool -from tests.conftest import needs_py39, needs_py310 +from tests.conftest import needs_py310 @pytest.fixture( name="module", params=[ - "tutorial001", - pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py index 1d0446ed41..cf24466c13 100644 --- a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py @@ -7,14 +7,13 @@ from sqlmodel import create_engine from sqlmodel.pool import StaticPool -from tests.conftest import needs_py39, needs_py310 +from tests.conftest import needs_py310 @pytest.fixture( name="module", params=[ - "tutorial001", - pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py index 186a120311..1ad004e939 100644 --- a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py @@ -7,14 +7,13 @@ from sqlmodel import create_engine from sqlmodel.pool import StaticPool -from tests.conftest import needs_py39, needs_py310 +from tests.conftest import needs_py310 @pytest.fixture( name="module", params=[ - "tutorial001", - pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py index 184fb219a9..6e441cccbc 100644 --- a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py @@ -7,14 +7,13 @@ from sqlmodel import create_engine from sqlmodel.pool import StaticPool -from tests.conftest import needs_py39, needs_py310 +from tests.conftest import needs_py310 @pytest.fixture( name="module", params=[ - "tutorial001", - pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py index dba7bc3879..b803cee28a 100644 --- a/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py @@ -13,7 +13,7 @@ @pytest.fixture( name="module", params=[ - "tutorial001", + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py index 2e7d66a036..3b028b7d91 100644 --- a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py @@ -7,14 +7,13 @@ from sqlmodel import create_engine from sqlmodel.pool import StaticPool -from tests.conftest import needs_py39, needs_py310 +from tests.conftest import needs_py310 @pytest.fixture( name="module", params=[ - "tutorial001", - pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py index b6ee27611f..9bc54fbcfb 100644 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py @@ -7,14 +7,13 @@ from sqlmodel import create_engine from sqlmodel.pool import StaticPool -from tests.conftest import needs_py39, needs_py310 +from tests.conftest import needs_py310 @pytest.fixture( name="module", params=[ - "tutorial001", - pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial002.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial002.py index a9312651b7..ede67fe4e4 100644 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial002.py +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial002.py @@ -7,14 +7,13 @@ from sqlmodel import Session, create_engine from sqlmodel.pool import StaticPool -from tests.conftest import needs_py39, needs_py310 +from tests.conftest import needs_py310 @pytest.fixture( name="module", params=[ - "tutorial002", - pytest.param("tutorial002_py39", marks=needs_py39), + pytest.param("tutorial002_py39"), pytest.param("tutorial002_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_indexes/test_tutorial001.py b/tests/test_tutorial/test_indexes/test_tutorial001.py index b71f7ebd85..8f031a8a32 100644 --- a/tests/test_tutorial/test_indexes/test_tutorial001.py +++ b/tests/test_tutorial/test_indexes/test_tutorial001.py @@ -12,7 +12,7 @@ @pytest.fixture( name="mod", params=[ - "tutorial001", + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_indexes/test_tutorial002.py b/tests/test_tutorial/test_indexes/test_tutorial002.py index 0aa88ea5dd..29b0987a40 100644 --- a/tests/test_tutorial/test_indexes/test_tutorial002.py +++ b/tests/test_tutorial/test_indexes/test_tutorial002.py @@ -12,7 +12,7 @@ @pytest.fixture( name="mod", params=[ - "tutorial002", + pytest.param("tutorial002_py39"), pytest.param("tutorial002_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_insert/test_tutorial001.py b/tests/test_tutorial/test_insert/test_tutorial001.py index 1673a70753..6e20feeb15 100644 --- a/tests/test_tutorial/test_insert/test_tutorial001.py +++ b/tests/test_tutorial/test_insert/test_tutorial001.py @@ -10,7 +10,7 @@ @pytest.fixture( name="mod", params=[ - "tutorial001", + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_insert/test_tutorial002.py b/tests/test_tutorial/test_insert/test_tutorial002.py index 0cee7ef3b1..b70ac63739 100644 --- a/tests/test_tutorial/test_insert/test_tutorial002.py +++ b/tests/test_tutorial/test_insert/test_tutorial002.py @@ -10,7 +10,7 @@ @pytest.fixture( name="mod", params=[ - "tutorial002", + pytest.param("tutorial002_py39"), pytest.param("tutorial002_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_insert/test_tutorial003.py b/tests/test_tutorial/test_insert/test_tutorial003.py index 7f80585875..902f5519d8 100644 --- a/tests/test_tutorial/test_insert/test_tutorial003.py +++ b/tests/test_tutorial/test_insert/test_tutorial003.py @@ -10,7 +10,7 @@ @pytest.fixture( name="mod", params=[ - "tutorial003", + pytest.param("tutorial003_py39"), pytest.param("tutorial003_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_limit_and_offset/test_tutorial001.py b/tests/test_tutorial/test_limit_and_offset/test_tutorial001.py index d33713cde3..b1b581e38e 100644 --- a/tests/test_tutorial/test_limit_and_offset/test_tutorial001.py +++ b/tests/test_tutorial/test_limit_and_offset/test_tutorial001.py @@ -10,7 +10,7 @@ @pytest.fixture( name="mod", params=[ - "tutorial001", + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_limit_and_offset/test_tutorial002.py b/tests/test_tutorial/test_limit_and_offset/test_tutorial002.py index c73f701cd1..896f2bfcec 100644 --- a/tests/test_tutorial/test_limit_and_offset/test_tutorial002.py +++ b/tests/test_tutorial/test_limit_and_offset/test_tutorial002.py @@ -10,7 +10,7 @@ @pytest.fixture( name="mod", params=[ - "tutorial002", + pytest.param("tutorial002_py39"), pytest.param("tutorial002_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_limit_and_offset/test_tutorial003.py b/tests/test_tutorial/test_limit_and_offset/test_tutorial003.py index a33cf49f06..8e9e0b7302 100644 --- a/tests/test_tutorial/test_limit_and_offset/test_tutorial003.py +++ b/tests/test_tutorial/test_limit_and_offset/test_tutorial003.py @@ -10,7 +10,7 @@ @pytest.fixture( name="mod", params=[ - "tutorial003", + pytest.param("tutorial003_py39"), pytest.param("tutorial003_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_limit_and_offset/test_tutorial004.py b/tests/test_tutorial/test_limit_and_offset/test_tutorial004.py index 15322c4089..228acd6aac 100644 --- a/tests/test_tutorial/test_limit_and_offset/test_tutorial004.py +++ b/tests/test_tutorial/test_limit_and_offset/test_tutorial004.py @@ -10,7 +10,7 @@ @pytest.fixture( name="mod", params=[ - "tutorial004", + pytest.param("tutorial004_py39"), pytest.param("tutorial004_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial001.py b/tests/test_tutorial/test_many_to_many/test_tutorial001.py index b84626d9cf..427fe41895 100644 --- a/tests/test_tutorial/test_many_to_many/test_tutorial001.py +++ b/tests/test_tutorial/test_many_to_many/test_tutorial001.py @@ -4,14 +4,13 @@ import pytest from sqlmodel import create_engine -from ...conftest import PrintMock, needs_py39, needs_py310 +from ...conftest import PrintMock, needs_py310 @pytest.fixture( name="mod", params=[ - "tutorial001", - pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial002.py b/tests/test_tutorial/test_many_to_many/test_tutorial002.py index b5562f416d..7ac120d5ae 100644 --- a/tests/test_tutorial/test_many_to_many/test_tutorial002.py +++ b/tests/test_tutorial/test_many_to_many/test_tutorial002.py @@ -4,14 +4,13 @@ import pytest from sqlmodel import create_engine -from ...conftest import PrintMock, needs_py39, needs_py310 +from ...conftest import PrintMock, needs_py310 @pytest.fixture( name="mod", params=[ - "tutorial002", - pytest.param("tutorial002_py39", marks=needs_py39), + pytest.param("tutorial002_py39"), pytest.param("tutorial002_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_many_to_many/test_tutorial003.py b/tests/test_tutorial/test_many_to_many/test_tutorial003.py index ad1168e3ba..6c14445b03 100644 --- a/tests/test_tutorial/test_many_to_many/test_tutorial003.py +++ b/tests/test_tutorial/test_many_to_many/test_tutorial003.py @@ -4,14 +4,13 @@ import pytest from sqlmodel import create_engine -from ...conftest import PrintMock, needs_py39, needs_py310 +from ...conftest import PrintMock, needs_py310 @pytest.fixture( name="mod", params=[ - "tutorial003", - pytest.param("tutorial003_py39", marks=needs_py39), + pytest.param("tutorial003_py39"), pytest.param("tutorial003_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_one/test_tutorial001.py b/tests/test_tutorial/test_one/test_tutorial001.py index 2f6f1c9e89..a4dd986ba0 100644 --- a/tests/test_tutorial/test_one/test_tutorial001.py +++ b/tests/test_tutorial/test_one/test_tutorial001.py @@ -10,7 +10,7 @@ @pytest.fixture( name="mod", params=[ - "tutorial001", + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_one/test_tutorial002.py b/tests/test_tutorial/test_one/test_tutorial002.py index 267ead8939..96dda11d2f 100644 --- a/tests/test_tutorial/test_one/test_tutorial002.py +++ b/tests/test_tutorial/test_one/test_tutorial002.py @@ -10,7 +10,7 @@ @pytest.fixture( name="mod", params=[ - "tutorial002", + pytest.param("tutorial002_py39"), pytest.param("tutorial002_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_one/test_tutorial003.py b/tests/test_tutorial/test_one/test_tutorial003.py index de2e529aea..5de20da912 100644 --- a/tests/test_tutorial/test_one/test_tutorial003.py +++ b/tests/test_tutorial/test_one/test_tutorial003.py @@ -10,7 +10,7 @@ @pytest.fixture( name="mod", params=[ - "tutorial003", + pytest.param("tutorial003_py39"), pytest.param("tutorial003_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_one/test_tutorial004.py b/tests/test_tutorial/test_one/test_tutorial004.py index 9df91fbb41..fce8277189 100644 --- a/tests/test_tutorial/test_one/test_tutorial004.py +++ b/tests/test_tutorial/test_one/test_tutorial004.py @@ -11,7 +11,7 @@ @pytest.fixture( name="mod", params=[ - "tutorial004", + pytest.param("tutorial004_py39"), pytest.param("tutorial004_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_one/test_tutorial005.py b/tests/test_tutorial/test_one/test_tutorial005.py index fa569020a1..a4f7914060 100644 --- a/tests/test_tutorial/test_one/test_tutorial005.py +++ b/tests/test_tutorial/test_one/test_tutorial005.py @@ -11,7 +11,7 @@ @pytest.fixture( name="mod", params=[ - "tutorial005", + pytest.param("tutorial005_py39"), pytest.param("tutorial005_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_one/test_tutorial006.py b/tests/test_tutorial/test_one/test_tutorial006.py index 1c6c663dc5..4ae19a5ee2 100644 --- a/tests/test_tutorial/test_one/test_tutorial006.py +++ b/tests/test_tutorial/test_one/test_tutorial006.py @@ -10,7 +10,7 @@ @pytest.fixture( name="mod", params=[ - "tutorial006", + pytest.param("tutorial006_py39"), pytest.param("tutorial006_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_one/test_tutorial007.py b/tests/test_tutorial/test_one/test_tutorial007.py index ca4a5310a5..edf674fef5 100644 --- a/tests/test_tutorial/test_one/test_tutorial007.py +++ b/tests/test_tutorial/test_one/test_tutorial007.py @@ -10,7 +10,7 @@ @pytest.fixture( name="mod", params=[ - "tutorial007", + pytest.param("tutorial007_py39"), pytest.param("tutorial007_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_one/test_tutorial008.py b/tests/test_tutorial/test_one/test_tutorial008.py index da451fbaa2..5a8e03a2d0 100644 --- a/tests/test_tutorial/test_one/test_tutorial008.py +++ b/tests/test_tutorial/test_one/test_tutorial008.py @@ -10,7 +10,7 @@ @pytest.fixture( name="mod", params=[ - "tutorial008", + pytest.param("tutorial008_py39"), pytest.param("tutorial008_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_one/test_tutorial009.py b/tests/test_tutorial/test_one/test_tutorial009.py index 0cf3a6267f..bfd1d3a2c3 100644 --- a/tests/test_tutorial/test_one/test_tutorial009.py +++ b/tests/test_tutorial/test_one/test_tutorial009.py @@ -10,7 +10,7 @@ @pytest.fixture( name="mod", params=[ - "tutorial009", + pytest.param("tutorial009_py39"), pytest.param("tutorial009_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001.py b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001.py index 698a7af2cb..208cfb7caa 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001.py +++ b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial001.py @@ -5,14 +5,13 @@ from sqlalchemy.exc import SAWarning from sqlmodel import create_engine -from ....conftest import PrintMock, needs_py39, needs_py310 +from ....conftest import PrintMock, needs_py310 @pytest.fixture( name="mod", params=[ - "tutorial001", - pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002.py b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002.py index 8ce54be46c..65a849f270 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002.py +++ b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial002.py @@ -4,14 +4,13 @@ import pytest from sqlmodel import create_engine -from ....conftest import PrintMock, needs_py39, needs_py310 +from ....conftest import PrintMock, needs_py310 @pytest.fixture( name="mod", params=[ - "tutorial002", - pytest.param("tutorial002_py39", marks=needs_py39), + pytest.param("tutorial002_py39"), pytest.param("tutorial002_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003.py b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003.py index d30c71c3c2..a23800db98 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003.py +++ b/tests/test_tutorial/test_relationship_attributes/test_back_populates/test_tutorial003.py @@ -6,14 +6,13 @@ from sqlalchemy.engine.reflection import Inspector from sqlmodel import create_engine -from ....conftest import needs_py39, needs_py310 +from ....conftest import needs_py310 @pytest.fixture( name="mod", params=[ - "tutorial003", - pytest.param("tutorial003_py39", marks=needs_py39), + pytest.param("tutorial003_py39"), pytest.param("tutorial003_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001.py b/tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001.py index 0c6a229178..d73be8f3c3 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001.py +++ b/tests/test_tutorial/test_relationship_attributes/test_create_and_update_relationships/test_tutorial001.py @@ -4,14 +4,13 @@ import pytest from sqlmodel import create_engine -from ....conftest import PrintMock, needs_py39, needs_py310 +from ....conftest import PrintMock, needs_py310 @pytest.fixture( name="mod", params=[ - "tutorial001", - pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001.py b/tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001.py index 96cf8bcc49..516f3e3843 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001.py +++ b/tests/test_tutorial/test_relationship_attributes/test_define_relationship_attributes/test_tutorial001.py @@ -4,14 +4,13 @@ import pytest from sqlmodel import create_engine -from ....conftest import PrintMock, needs_py39, needs_py310 +from ....conftest import PrintMock, needs_py310 @pytest.fixture( name="mod", params=[ - "tutorial001", - pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001.py index 5e43f80c4c..fddaf3d9bf 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001.py +++ b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial001.py @@ -4,14 +4,13 @@ import pytest from sqlmodel import create_engine -from ....conftest import PrintMock, needs_py39, needs_py310 +from ....conftest import PrintMock, needs_py310 @pytest.fixture( name="mod", params=[ - "tutorial001", - pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002.py index cb3907afd7..5614c2fecc 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002.py +++ b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial002.py @@ -4,14 +4,13 @@ import pytest from sqlmodel import create_engine -from ....conftest import PrintMock, needs_py39, needs_py310 +from ....conftest import PrintMock, needs_py310 @pytest.fixture( name="mod", params=[ - "tutorial002", - pytest.param("tutorial002_py39", marks=needs_py39), + pytest.param("tutorial002_py39"), pytest.param("tutorial002_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003.py index e063630f96..095062d7d7 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003.py +++ b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial003.py @@ -4,14 +4,13 @@ import pytest from sqlmodel import create_engine -from ....conftest import PrintMock, needs_py39, needs_py310 +from ....conftest import PrintMock, needs_py310 @pytest.fixture( name="mod", params=[ - "tutorial003", - pytest.param("tutorial003_py39", marks=needs_py39), + pytest.param("tutorial003_py39"), pytest.param("tutorial003_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004.py index 8b6c101fb8..fb045d45a7 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004.py +++ b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial004.py @@ -5,14 +5,13 @@ from sqlalchemy.exc import IntegrityError from sqlmodel import Session, create_engine, select -from ....conftest import PrintMock, needs_py39, needs_py310 +from ....conftest import PrintMock, needs_py310 @pytest.fixture( name="mod", params=[ - "tutorial004", - pytest.param("tutorial004_py39", marks=needs_py39), + pytest.param("tutorial004_py39"), pytest.param("tutorial004_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005.py b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005.py index 273007770b..d2170f225c 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005.py +++ b/tests/test_tutorial/test_relationship_attributes/test_delete_records_relationship/test_tutorial005.py @@ -4,14 +4,13 @@ import pytest from sqlmodel import create_engine -from ....conftest import PrintMock, needs_py39, needs_py310 +from ....conftest import PrintMock, needs_py310 @pytest.fixture( name="mod", params=[ - "tutorial005", - pytest.param("tutorial005_py39", marks=needs_py39), + pytest.param("tutorial005_py39"), pytest.param("tutorial005_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001.py b/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001.py index 1b2caaa9c5..651f44a033 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001.py +++ b/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial001.py @@ -4,14 +4,13 @@ import pytest from sqlmodel import create_engine -from ....conftest import PrintMock, needs_py39, needs_py310 +from ....conftest import PrintMock, needs_py310 @pytest.fixture( name="mod", params=[ - "tutorial001", - pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002.py b/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002.py index 68ee2cc1d8..306f3f174b 100644 --- a/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002.py +++ b/tests/test_tutorial/test_relationship_attributes/test_read_relationships/test_tutorial002.py @@ -4,14 +4,13 @@ import pytest from sqlmodel import create_engine -from ....conftest import PrintMock, needs_py39, needs_py310 +from ....conftest import PrintMock, needs_py310 @pytest.fixture( name="mod", params=[ - "tutorial002", - pytest.param("tutorial002_py39", marks=needs_py39), + pytest.param("tutorial002_py39"), pytest.param("tutorial002_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_select/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_select/test_tutorial001_tutorial002.py index a3f5e90d46..3dc5c186c0 100644 --- a/tests/test_tutorial/test_select/test_tutorial001_tutorial002.py +++ b/tests/test_tutorial/test_select/test_tutorial001_tutorial002.py @@ -40,7 +40,7 @@ def get_module(request: pytest.FixtureRequest) -> ModuleType: @pytest.mark.parametrize( "module", [ - "tutorial001", + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), ], indirect=True, @@ -53,7 +53,7 @@ def test_tutorial_001(print_mock: PrintMock, module: ModuleType): @pytest.mark.parametrize( "module", [ - "tutorial002", + pytest.param("tutorial002_py39"), pytest.param("tutorial002_py310", marks=needs_py310), ], indirect=True, diff --git a/tests/test_tutorial/test_select/test_tutorial003_tutorial004.py b/tests/test_tutorial/test_select/test_tutorial003_tutorial004.py index 967ecdbd50..e64d5f8b79 100644 --- a/tests/test_tutorial/test_select/test_tutorial003_tutorial004.py +++ b/tests/test_tutorial/test_select/test_tutorial003_tutorial004.py @@ -42,7 +42,7 @@ def get_module(request: pytest.FixtureRequest) -> ModuleType: @pytest.mark.parametrize( "module", [ - "tutorial003", + pytest.param("tutorial003_py39"), pytest.param("tutorial003_py310", marks=needs_py310), ], indirect=True, @@ -55,7 +55,7 @@ def test_tutorial_003(print_mock: PrintMock, module: ModuleType): @pytest.mark.parametrize( "module", [ - "tutorial004", + pytest.param("tutorial004_py39"), pytest.param("tutorial004_py310", marks=needs_py310), ], indirect=True, diff --git a/tests/test_tutorial/test_update/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_update/test_tutorial001_tutorial002.py index ef12a8d950..6f4b05ee1d 100644 --- a/tests/test_tutorial/test_update/test_tutorial001_tutorial002.py +++ b/tests/test_tutorial/test_update/test_tutorial001_tutorial002.py @@ -39,7 +39,7 @@ def get_module(request: pytest.FixtureRequest) -> ModuleType: @pytest.mark.parametrize( "module", [ - "tutorial001", + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), ], indirect=True, @@ -52,7 +52,7 @@ def test_tutorial001(print_mock: PrintMock, module: ModuleType): @pytest.mark.parametrize( "module", [ - "tutorial002", + pytest.param("tutorial002_py39"), pytest.param("tutorial002_py310", marks=needs_py310), ], indirect=True, diff --git a/tests/test_tutorial/test_update/test_tutorial003_tutorial004.py b/tests/test_tutorial/test_update/test_tutorial003_tutorial004.py index 76788c62cd..944b2ebf06 100644 --- a/tests/test_tutorial/test_update/test_tutorial003_tutorial004.py +++ b/tests/test_tutorial/test_update/test_tutorial003_tutorial004.py @@ -52,7 +52,7 @@ def get_module(request: pytest.FixtureRequest) -> ModuleType: @pytest.mark.parametrize( "module", [ - "tutorial003", + pytest.param("tutorial003_py39"), pytest.param("tutorial003_py310", marks=needs_py310), ], indirect=True, @@ -65,7 +65,7 @@ def test_tutorial003(print_mock: PrintMock, module: ModuleType): @pytest.mark.parametrize( "module", [ - "tutorial004", + pytest.param("tutorial004_py39"), pytest.param("tutorial004_py310", marks=needs_py310), ], indirect=True, diff --git a/tests/test_tutorial/test_where/test_tutorial001.py b/tests/test_tutorial/test_where/test_tutorial001.py index 1a557deefc..a6b869d81e 100644 --- a/tests/test_tutorial/test_where/test_tutorial001.py +++ b/tests/test_tutorial/test_where/test_tutorial001.py @@ -10,7 +10,7 @@ @pytest.fixture( name="mod", params=[ - "tutorial001", + pytest.param("tutorial001_py39"), pytest.param("tutorial001_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_where/test_tutorial002.py b/tests/test_tutorial/test_where/test_tutorial002.py index 1c96f76126..7c6812fb2b 100644 --- a/tests/test_tutorial/test_where/test_tutorial002.py +++ b/tests/test_tutorial/test_where/test_tutorial002.py @@ -10,7 +10,7 @@ @pytest.fixture( name="mod", params=[ - "tutorial002", + pytest.param("tutorial002_py39"), pytest.param("tutorial002_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_where/test_tutorial003.py b/tests/test_tutorial/test_where/test_tutorial003.py index 6e90d22fc4..304310a77a 100644 --- a/tests/test_tutorial/test_where/test_tutorial003.py +++ b/tests/test_tutorial/test_where/test_tutorial003.py @@ -10,7 +10,7 @@ @pytest.fixture( name="mod", params=[ - "tutorial003", + pytest.param("tutorial003_py39"), pytest.param("tutorial003_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_where/test_tutorial004.py b/tests/test_tutorial/test_where/test_tutorial004.py index b7a1212b77..bace062f67 100644 --- a/tests/test_tutorial/test_where/test_tutorial004.py +++ b/tests/test_tutorial/test_where/test_tutorial004.py @@ -10,7 +10,7 @@ @pytest.fixture( name="mod", params=[ - "tutorial004", + pytest.param("tutorial004_py39"), pytest.param("tutorial004_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_where/test_tutorial005.py b/tests/test_tutorial/test_where/test_tutorial005.py index 9adbec74a2..39f7b1b2bc 100644 --- a/tests/test_tutorial/test_where/test_tutorial005.py +++ b/tests/test_tutorial/test_where/test_tutorial005.py @@ -10,7 +10,7 @@ @pytest.fixture( name="mod", params=[ - "tutorial005", + pytest.param("tutorial005_py39"), pytest.param("tutorial005_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_where/test_tutorial006.py b/tests/test_tutorial/test_where/test_tutorial006.py index e5d586a39a..9830142ad0 100644 --- a/tests/test_tutorial/test_where/test_tutorial006.py +++ b/tests/test_tutorial/test_where/test_tutorial006.py @@ -10,7 +10,7 @@ @pytest.fixture( name="mod", params=[ - "tutorial006", + pytest.param("tutorial006_py39"), pytest.param("tutorial006_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_where/test_tutorial007.py b/tests/test_tutorial/test_where/test_tutorial007.py index 9a36279d38..c2650193da 100644 --- a/tests/test_tutorial/test_where/test_tutorial007.py +++ b/tests/test_tutorial/test_where/test_tutorial007.py @@ -10,7 +10,7 @@ @pytest.fixture( name="mod", params=[ - "tutorial007", + pytest.param("tutorial007_py39"), pytest.param("tutorial007_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_where/test_tutorial008.py b/tests/test_tutorial/test_where/test_tutorial008.py index a21e2ecbe3..ac5ccf0553 100644 --- a/tests/test_tutorial/test_where/test_tutorial008.py +++ b/tests/test_tutorial/test_where/test_tutorial008.py @@ -10,7 +10,7 @@ @pytest.fixture( name="mod", params=[ - "tutorial008", + pytest.param("tutorial008_py39"), pytest.param("tutorial008_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_where/test_tutorial009.py b/tests/test_tutorial/test_where/test_tutorial009.py index 8088045b02..22b359768e 100644 --- a/tests/test_tutorial/test_where/test_tutorial009.py +++ b/tests/test_tutorial/test_where/test_tutorial009.py @@ -10,7 +10,7 @@ @pytest.fixture( name="mod", params=[ - "tutorial009", + pytest.param("tutorial009_py39"), pytest.param("tutorial009_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_where/test_tutorial010.py b/tests/test_tutorial/test_where/test_tutorial010.py index ea235ef708..8740de1a7c 100644 --- a/tests/test_tutorial/test_where/test_tutorial010.py +++ b/tests/test_tutorial/test_where/test_tutorial010.py @@ -10,7 +10,7 @@ @pytest.fixture( name="mod", params=[ - "tutorial010", + pytest.param("tutorial010_py39"), pytest.param("tutorial010_py310", marks=needs_py310), ], ) diff --git a/tests/test_tutorial/test_where/test_tutorial011.py b/tests/test_tutorial/test_where/test_tutorial011.py index ba53550611..8131e2adc1 100644 --- a/tests/test_tutorial/test_where/test_tutorial011.py +++ b/tests/test_tutorial/test_where/test_tutorial011.py @@ -10,7 +10,7 @@ @pytest.fixture( name="mod", params=[ - "tutorial011", + pytest.param("tutorial011_py39"), pytest.param("tutorial011_py310", marks=needs_py310), ], ) From 028ad292033b663792a3d88d290b18a0e79decb8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Dec 2025 11:03:26 +0000 Subject: [PATCH 884/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 9ddf7ed7c0..fadb7658a8 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Docs + +* ➖ Drop support for Python 3.8 in CI and docs. PR [#1695](https://github.com/fastapi/sqlmodel/pull/1695) by [@tiangolo](https://github.com/tiangolo). + ### Internal * ⬆ Bump actions/checkout from 5 to 6. PR [#1692](https://github.com/fastapi/sqlmodel/pull/1692) by [@dependabot[bot]](https://github.com/apps/dependabot). From 056232ab4518de2f4b0a1375985e9a81904cad63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 26 Dec 2025 03:23:00 -0800 Subject: [PATCH 885/906] =?UTF-8?q?=E2=9E=96=20Drop=20support=20for=20Pyth?= =?UTF-8?q?on=203.8=20(#1696)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pdm_build.py | 14 +-- pyproject.toml | 3 +- scripts/generate_select.py | 9 +- scripts/mkdocs_hooks.py | 8 +- sqlmodel/_compat.py | 68 +++++----- sqlmodel/ext/asyncio/session.py | 17 ++- sqlmodel/main.py | 118 ++++++++---------- sqlmodel/orm/session.py | 14 +-- sqlmodel/sql/_expression_select_cls.py | 3 +- sqlmodel/sql/_expression_select_gen.py | 63 +++++----- sqlmodel/sql/_expression_select_gen.py.jinja2 | 9 +- sqlmodel/sql/expression.py | 16 +-- tests/conftest.py | 9 +- tests/test_aliases.py | 26 ++-- tests/test_field_sa_relationship.py | 6 +- tests/test_main.py | 4 +- tests/test_ondelete_raises.py | 4 +- tests/test_sqlalchemy_type_errors.py | 6 +- .../test_tutorial001_tutorial002.py | 4 +- .../test_tutorial001_tutorial002.py | 4 +- .../test_tutorial003_tutorial004.py | 4 +- 21 files changed, 185 insertions(+), 224 deletions(-) diff --git a/pdm_build.py b/pdm_build.py index 2324670159..33a6b267e4 100644 --- a/pdm_build.py +++ b/pdm_build.py @@ -1,5 +1,5 @@ import os -from typing import Any, Dict, List +from typing import Any from pdm.backend.hooks import Context @@ -9,30 +9,30 @@ def pdm_build_initialize(context: Context) -> None: metadata = context.config.metadata # Get custom config for the current package, from the env var - config: Dict[str, Any] = context.config.data["tool"]["tiangolo"][ + config: dict[str, Any] = context.config.data["tool"]["tiangolo"][ "_internal-slim-build" ]["packages"][TIANGOLO_BUILD_PACKAGE] - project_config: Dict[str, Any] = config["project"] + project_config: dict[str, Any] = config["project"] # Get main optional dependencies, extras - optional_dependencies: Dict[str, List[str]] = metadata.get( + optional_dependencies: dict[str, list[str]] = metadata.get( "optional-dependencies", {} ) # Get custom optional dependencies name to always include in this (non-slim) package - include_optional_dependencies: List[str] = config.get( + include_optional_dependencies: list[str] = config.get( "include-optional-dependencies", [] ) # Override main [project] configs with custom configs for this package for key, value in project_config.items(): metadata[key] = value # Get custom build config for the current package - build_config: Dict[str, Any] = ( + build_config: dict[str, Any] = ( config.get("tool", {}).get("pdm", {}).get("build", {}) ) # Override PDM build config with custom build config for this package for key, value in build_config.items(): context.config.build_config[key] = value # Get main dependencies - dependencies: List[str] = metadata.get("dependencies", []) + dependencies: list[str] = metadata.get("dependencies", []) # Add optional dependencies to the default dependencies for this (non-slim) package for include_optional in include_optional_dependencies: optional_dependencies_group = optional_dependencies.get(include_optional, []) diff --git a/pyproject.toml b/pyproject.toml index 048f38844c..0a95bfab13 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "sqlmodel" dynamic = ["version"] description = "SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness." readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.9" authors = [ { name = "Sebastián Ramírez", email = "tiangolo@gmail.com" }, ] @@ -21,7 +21,6 @@ classifiers = [ "Intended Audience :: Science/Research", "Intended Audience :: System Administrators", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", diff --git a/scripts/generate_select.py b/scripts/generate_select.py index 49c2b2b015..cbb842b367 100644 --- a/scripts/generate_select.py +++ b/scripts/generate_select.py @@ -1,7 +1,6 @@ import os from itertools import product from pathlib import Path -from typing import List, Tuple import black from jinja2 import Template @@ -21,15 +20,15 @@ class Arg(BaseModel): annotation: str -arg_groups: List[Arg] = [] +arg_groups: list[Arg] = [] -signatures: List[Tuple[List[Arg], List[str]]] = [] +signatures: list[tuple[list[Arg], list[str]]] = [] for total_args in range(2, number_of_types + 1): arg_types_tuples = product(["model", "scalar"], repeat=total_args) for arg_type_tuple in arg_types_tuples: - args: List[Arg] = [] - return_types: List[str] = [] + args: list[Arg] = [] + return_types: list[str] = [] for i, arg_type in enumerate(arg_type_tuple): if arg_type == "scalar": t_var = f"_TScalar_{i}" diff --git a/scripts/mkdocs_hooks.py b/scripts/mkdocs_hooks.py index f09e9a99df..0e8789461b 100644 --- a/scripts/mkdocs_hooks.py +++ b/scripts/mkdocs_hooks.py @@ -1,4 +1,4 @@ -from typing import Any, List, Union +from typing import Any, Union from mkdocs.config.defaults import MkDocsConfig from mkdocs.structure.files import Files @@ -7,9 +7,9 @@ def generate_renamed_section_items( - items: List[Union[Page, Section, Link]], *, config: MkDocsConfig -) -> List[Union[Page, Section, Link]]: - new_items: List[Union[Page, Section, Link]] = [] + items: list[Union[Page, Section, Link]], *, config: MkDocsConfig +) -> list[Union[Page, Section, Link]]: + new_items: list[Union[Page, Section, Link]] = [] for item in items: if isinstance(item, Section): new_title = item.title diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index 230f8cc362..60874149bf 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -1,20 +1,16 @@ import sys import types +from collections.abc import Generator, Mapping, Set from contextlib import contextmanager from contextvars import ContextVar from dataclasses import dataclass from typing import ( TYPE_CHECKING, - AbstractSet, + Annotated, Any, Callable, - Dict, ForwardRef, - Generator, - Mapping, Optional, - Set, - Type, TypeVar, Union, ) @@ -22,7 +18,7 @@ from pydantic import VERSION as P_VERSION from pydantic import BaseModel from pydantic.fields import FieldInfo -from typing_extensions import Annotated, get_args, get_origin +from typing_extensions import get_args, get_origin # Reassign variable to make it reexported for mypy PYDANTIC_VERSION = P_VERSION @@ -36,7 +32,7 @@ UnionType = getattr(types, "UnionType", Union) NoneType = type(None) T = TypeVar("T") -InstanceOrType = Union[T, Type[T]] +InstanceOrType = Union[T, type[T]] _TSQLModel = TypeVar("_TSQLModel", bound="SQLModel") @@ -49,7 +45,7 @@ class FakeMetadata: @dataclass class ObjectWithUpdateWrapper: obj: Any - update: Dict[str, Any] + update: dict[str, Any] def __getattribute__(self, __name: str) -> Any: update = super().__getattribute__("update") @@ -103,7 +99,7 @@ def set_config_value( ) -> None: model.model_config[parameter] = value # type: ignore[literal-required] - def get_model_fields(model: InstanceOrType[BaseModel]) -> Dict[str, "FieldInfo"]: + def get_model_fields(model: InstanceOrType[BaseModel]) -> dict[str, "FieldInfo"]: # TODO: refactor the usage of this function to always pass the class # not the instance, and then remove this extra check # this is for compatibility with Pydantic v3 @@ -115,7 +111,7 @@ def get_model_fields(model: InstanceOrType[BaseModel]) -> Dict[str, "FieldInfo"] def get_fields_set( object: InstanceOrType["SQLModel"], - ) -> Union[Set[str], Callable[[BaseModel], Set[str]]]: + ) -> Union[set[str], Callable[[BaseModel], set[str]]]: return object.model_fields_set def init_pydantic_private_attrs(new_object: InstanceOrType["SQLModel"]) -> None: @@ -123,8 +119,8 @@ def init_pydantic_private_attrs(new_object: InstanceOrType["SQLModel"]) -> None: object.__setattr__(new_object, "__pydantic_extra__", None) object.__setattr__(new_object, "__pydantic_private__", None) - def get_annotations(class_dict: Dict[str, Any]) -> Dict[str, Any]: - raw_annotations: Dict[str, Any] = class_dict.get("__annotations__", {}) + def get_annotations(class_dict: dict[str, Any]) -> dict[str, Any]: + raw_annotations: dict[str, Any] = class_dict.get("__annotations__", {}) if sys.version_info >= (3, 14) and "__annotations__" not in class_dict: # See https://github.com/pydantic/pydantic/pull/11991 from annotationlib import ( @@ -139,7 +135,7 @@ def get_annotations(class_dict: Dict[str, Any]) -> Dict[str, Any]: ) return raw_annotations - def is_table_model_class(cls: Type[Any]) -> bool: + def is_table_model_class(cls: type[Any]) -> bool: config = getattr(cls, "model_config", {}) if config: return config.get("table", False) or False @@ -243,15 +239,15 @@ def _calculate_keys( include: Optional[Mapping[Union[int, str], Any]], exclude: Optional[Mapping[Union[int, str], Any]], exclude_unset: bool, - update: Optional[Dict[str, Any]] = None, - ) -> Optional[AbstractSet[str]]: # pragma: no cover + update: Optional[dict[str, Any]] = None, + ) -> Optional[Set[str]]: # pragma: no cover return None def sqlmodel_table_construct( *, self_instance: _TSQLModel, - values: Dict[str, Any], - _fields_set: Union[Set[str], None] = None, + values: dict[str, Any], + _fields_set: Union[set[str], None] = None, ) -> _TSQLModel: # Copy from Pydantic's BaseModel.construct() # Ref: https://github.com/pydantic/pydantic/blob/v2.5.2/pydantic/main.py#L198 @@ -264,8 +260,8 @@ def sqlmodel_table_construct( old_dict = self_instance.__dict__.copy() # End SQLModel override - fields_values: Dict[str, Any] = {} - defaults: Dict[ + fields_values: dict[str, Any] = {} + defaults: dict[ str, Any ] = {} # keeping this separate from `fields_values` helps us compute `_fields_set` for name, field in cls.model_fields.items(): @@ -279,7 +275,7 @@ def sqlmodel_table_construct( _fields_set = set(fields_values.keys()) fields_values.update(defaults) - _extra: Union[Dict[str, Any], None] = None + _extra: Union[dict[str, Any], None] = None if cls.model_config.get("extra") == "allow": _extra = {} for k, v in values.items(): @@ -315,13 +311,13 @@ def sqlmodel_table_construct( return self_instance def sqlmodel_validate( - cls: Type[_TSQLModel], + cls: type[_TSQLModel], obj: Any, *, strict: Union[bool, None] = None, from_attributes: Union[bool, None] = None, - context: Union[Dict[str, Any], None] = None, - update: Union[Dict[str, Any], None] = None, + context: Union[dict[str, Any], None] = None, + update: Union[dict[str, Any], None] = None, ) -> _TSQLModel: if not is_table_model_class(cls): new_obj: _TSQLModel = cls.__new__(cls) @@ -366,7 +362,7 @@ def sqlmodel_validate( setattr(new_obj, key, value) return new_obj - def sqlmodel_init(*, self: "SQLModel", data: Dict[str, Any]) -> None: + def sqlmodel_init(*, self: "SQLModel", data: dict[str, Any]) -> None: old_dict = self.__dict__.copy() if not is_table_model_class(self.__class__): self.__pydantic_validator__.validate_python( @@ -424,24 +420,24 @@ def set_config_value( ) -> None: setattr(model.__config__, parameter, value) # type: ignore - def get_model_fields(model: InstanceOrType[BaseModel]) -> Dict[str, "FieldInfo"]: + def get_model_fields(model: InstanceOrType[BaseModel]) -> dict[str, "FieldInfo"]: return model.__fields__ # type: ignore def get_fields_set( object: InstanceOrType["SQLModel"], - ) -> Union[Set[str], Callable[[BaseModel], Set[str]]]: + ) -> Union[set[str], Callable[[BaseModel], set[str]]]: return object.__fields_set__ def init_pydantic_private_attrs(new_object: InstanceOrType["SQLModel"]) -> None: object.__setattr__(new_object, "__fields_set__", set()) - def get_annotations(class_dict: Dict[str, Any]) -> Dict[str, Any]: + def get_annotations(class_dict: dict[str, Any]) -> dict[str, Any]: return resolve_annotations( # type: ignore[no-any-return] class_dict.get("__annotations__", {}), class_dict.get("__module__", None), ) - def is_table_model_class(cls: Type[Any]) -> bool: + def is_table_model_class(cls: type[Any]) -> bool: config = getattr(cls, "__config__", None) if config: return getattr(config, "table", False) @@ -492,8 +488,8 @@ def _calculate_keys( include: Optional[Mapping[Union[int, str], Any]], exclude: Optional[Mapping[Union[int, str], Any]], exclude_unset: bool, - update: Optional[Dict[str, Any]] = None, - ) -> Optional[AbstractSet[str]]: + update: Optional[dict[str, Any]] = None, + ) -> Optional[Set[str]]: if include is None and exclude is None and not exclude_unset: # Original in Pydantic: # return None @@ -504,7 +500,7 @@ def _calculate_keys( self.__fields__.keys() # noqa ) # | self.__sqlmodel_relationships__.keys() - keys: AbstractSet[str] + keys: Set[str] if exclude_unset: keys = self.__fields_set__.copy() # noqa else: @@ -528,13 +524,13 @@ def _calculate_keys( return keys def sqlmodel_validate( - cls: Type[_TSQLModel], + cls: type[_TSQLModel], obj: Any, *, strict: Union[bool, None] = None, from_attributes: Union[bool, None] = None, - context: Union[Dict[str, Any], None] = None, - update: Union[Dict[str, Any], None] = None, + context: Union[dict[str, Any], None] = None, + update: Union[dict[str, Any], None] = None, ) -> _TSQLModel: # This was SQLModel's original from_orm() for Pydantic v1 # Duplicated from Pydantic @@ -573,7 +569,7 @@ def sqlmodel_validate( m._init_private_attributes() # type: ignore[attr-defined] # noqa return m - def sqlmodel_init(*, self: "SQLModel", data: Dict[str, Any]) -> None: + def sqlmodel_init(*, self: "SQLModel", data: dict[str, Any]) -> None: values, fields_set, validation_error = validate_model(self.__class__, data) # Only raise errors if not a SQLModel model if ( diff --git a/sqlmodel/ext/asyncio/session.py b/sqlmodel/ext/asyncio/session.py index ff99dff899..056737f866 100644 --- a/sqlmodel/ext/asyncio/session.py +++ b/sqlmodel/ext/asyncio/session.py @@ -1,10 +1,7 @@ +from collections.abc import Mapping, Sequence from typing import ( Any, - Dict, - Mapping, Optional, - Sequence, - Type, TypeVar, Union, cast, @@ -32,7 +29,7 @@ class AsyncSession(_AsyncSession): - sync_session_class: Type[Session] = Session + sync_session_class: type[Session] = Session sync_session: Session @overload @@ -42,7 +39,7 @@ async def exec( *, params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, execution_options: Mapping[str, Any] = util.EMPTY_DICT, - bind_arguments: Optional[Dict[str, Any]] = None, + bind_arguments: Optional[dict[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, ) -> TupleResult[_TSelectParam]: ... @@ -54,7 +51,7 @@ async def exec( *, params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, execution_options: Mapping[str, Any] = util.EMPTY_DICT, - bind_arguments: Optional[Dict[str, Any]] = None, + bind_arguments: Optional[dict[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, ) -> ScalarResult[_TSelectParam]: ... @@ -66,7 +63,7 @@ async def exec( *, params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, execution_options: Mapping[str, Any] = util.EMPTY_DICT, - bind_arguments: Optional[Dict[str, Any]] = None, + bind_arguments: Optional[dict[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, ) -> CursorResult[Any]: ... @@ -82,7 +79,7 @@ async def exec( *, params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, execution_options: Mapping[str, Any] = util.EMPTY_DICT, - bind_arguments: Optional[Dict[str, Any]] = None, + bind_arguments: Optional[dict[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, ) -> Union[ @@ -135,7 +132,7 @@ async def execute( params: Optional[_CoreAnyExecuteParams] = None, *, execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT, - bind_arguments: Optional[Dict[str, Any]] = None, + bind_arguments: Optional[dict[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, ) -> Result[Any]: diff --git a/sqlmodel/main.py b/sqlmodel/main.py index d301694260..2e558647f1 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -1,26 +1,20 @@ from __future__ import annotations +import builtins import ipaddress import uuid import weakref +from collections.abc import Mapping, Sequence, Set from datetime import date, datetime, time, timedelta from decimal import Decimal from enum import Enum from pathlib import Path from typing import ( TYPE_CHECKING, - AbstractSet, Any, Callable, ClassVar, - Dict, - List, - Mapping, Optional, - Sequence, - Set, - Tuple, - Type, TypeVar, Union, cast, @@ -93,8 +87,8 @@ _T = TypeVar("_T") NoArgAnyCallable = Callable[[], Any] IncEx: TypeAlias = Union[ - Set[int], - Set[str], + set[int], + set[str], Mapping[int, Union["IncEx", bool]], Mapping[str, Union["IncEx", bool]], ] @@ -106,7 +100,7 @@ def __dataclass_transform__( eq_default: bool = True, order_default: bool = False, kw_only_default: bool = False, - field_descriptors: Tuple[Union[type, Callable[..., Any]], ...] = (()), + field_descriptors: tuple[Union[type, Callable[..., Any]], ...] = (()), ) -> Callable[[_T], _T]: return lambda a: a @@ -222,12 +216,8 @@ def Field( serialization_alias: Optional[str] = None, title: Optional[str] = None, description: Optional[str] = None, - exclude: Union[ - AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any - ] = None, - include: Union[ - AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any - ] = None, + exclude: Union[Set[Union[int, str]], Mapping[Union[int, str], Any], Any] = None, + include: Union[Set[Union[int, str]], Mapping[Union[int, str], Any], Any] = None, const: Optional[bool] = None, gt: Optional[float] = None, ge: Optional[float] = None, @@ -250,10 +240,10 @@ def Field( unique: Union[bool, UndefinedType] = Undefined, nullable: Union[bool, UndefinedType] = Undefined, index: Union[bool, UndefinedType] = Undefined, - sa_type: Union[Type[Any], UndefinedType] = Undefined, + sa_type: Union[type[Any], UndefinedType] = Undefined, sa_column_args: Union[Sequence[Any], UndefinedType] = Undefined, sa_column_kwargs: Union[Mapping[str, Any], UndefinedType] = Undefined, - schema_extra: Optional[Dict[str, Any]] = None, + schema_extra: Optional[dict[str, Any]] = None, ) -> Any: ... @@ -269,12 +259,8 @@ def Field( serialization_alias: Optional[str] = None, title: Optional[str] = None, description: Optional[str] = None, - exclude: Union[ - AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any - ] = None, - include: Union[ - AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any - ] = None, + exclude: Union[Set[Union[int, str]], Mapping[Union[int, str], Any], Any] = None, + include: Union[Set[Union[int, str]], Mapping[Union[int, str], Any], Any] = None, const: Optional[bool] = None, gt: Optional[float] = None, ge: Optional[float] = None, @@ -298,10 +284,10 @@ def Field( unique: Union[bool, UndefinedType] = Undefined, nullable: Union[bool, UndefinedType] = Undefined, index: Union[bool, UndefinedType] = Undefined, - sa_type: Union[Type[Any], UndefinedType] = Undefined, + sa_type: Union[type[Any], UndefinedType] = Undefined, sa_column_args: Union[Sequence[Any], UndefinedType] = Undefined, sa_column_kwargs: Union[Mapping[str, Any], UndefinedType] = Undefined, - schema_extra: Optional[Dict[str, Any]] = None, + schema_extra: Optional[dict[str, Any]] = None, ) -> Any: ... @@ -325,12 +311,8 @@ def Field( serialization_alias: Optional[str] = None, title: Optional[str] = None, description: Optional[str] = None, - exclude: Union[ - AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any - ] = None, - include: Union[ - AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any - ] = None, + exclude: Union[Set[Union[int, str]], Mapping[Union[int, str], Any], Any] = None, + include: Union[Set[Union[int, str]], Mapping[Union[int, str], Any], Any] = None, const: Optional[bool] = None, gt: Optional[float] = None, ge: Optional[float] = None, @@ -349,7 +331,7 @@ def Field( discriminator: Optional[str] = None, repr: bool = True, sa_column: Union[Column[Any], UndefinedType] = Undefined, - schema_extra: Optional[Dict[str, Any]] = None, + schema_extra: Optional[dict[str, Any]] = None, ) -> Any: ... @@ -362,12 +344,8 @@ def Field( serialization_alias: Optional[str] = None, title: Optional[str] = None, description: Optional[str] = None, - exclude: Union[ - AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any - ] = None, - include: Union[ - AbstractSet[Union[int, str]], Mapping[Union[int, str], Any], Any - ] = None, + exclude: Union[Set[Union[int, str]], Mapping[Union[int, str], Any], Any] = None, + include: Union[Set[Union[int, str]], Mapping[Union[int, str], Any], Any] = None, const: Optional[bool] = None, gt: Optional[float] = None, ge: Optional[float] = None, @@ -391,11 +369,11 @@ def Field( unique: Union[bool, UndefinedType] = Undefined, nullable: Union[bool, UndefinedType] = Undefined, index: Union[bool, UndefinedType] = Undefined, - sa_type: Union[Type[Any], UndefinedType] = Undefined, + sa_type: Union[type[Any], UndefinedType] = Undefined, sa_column: Union[Column, UndefinedType] = Undefined, # type: ignore sa_column_args: Union[Sequence[Any], UndefinedType] = Undefined, sa_column_kwargs: Union[Mapping[str, Any], UndefinedType] = Undefined, - schema_extra: Optional[Dict[str, Any]] = None, + schema_extra: Optional[dict[str, Any]] = None, ) -> Any: current_schema_extra = schema_extra or {} # Extract possible alias settings from schema_extra so we can control precedence @@ -506,11 +484,11 @@ def Relationship( @__dataclass_transform__(kw_only_default=True, field_descriptors=(Field, FieldInfo)) class SQLModelMetaclass(ModelMetaclass, DeclarativeMeta): - __sqlmodel_relationships__: Dict[str, RelationshipInfo] + __sqlmodel_relationships__: dict[str, RelationshipInfo] model_config: SQLModelConfig - model_fields: ClassVar[Dict[str, FieldInfo]] - __config__: Type[SQLModelConfig] - __fields__: Dict[str, ModelField] # type: ignore[assignment] + model_fields: ClassVar[dict[str, FieldInfo]] + __config__: type[SQLModelConfig] + __fields__: dict[str, ModelField] # type: ignore[assignment] # Replicate SQLAlchemy def __setattr__(cls, name: str, value: Any) -> None: @@ -529,11 +507,11 @@ def __delattr__(cls, name: str) -> None: def __new__( cls, name: str, - bases: Tuple[Type[Any], ...], - class_dict: Dict[str, Any], + bases: tuple[type[Any], ...], + class_dict: dict[str, Any], **kwargs: Any, ) -> Any: - relationships: Dict[str, RelationshipInfo] = {} + relationships: dict[str, RelationshipInfo] = {} dict_for_pydantic = {} original_annotations = get_annotations(class_dict) pydantic_annotations = {} @@ -557,7 +535,7 @@ def __new__( # Duplicate logic from Pydantic to filter config kwargs because if they are # passed directly including the registry Pydantic will pass them over to the # superclass causing an error - allowed_config_kwargs: Set[str] = { + allowed_config_kwargs: set[str] = { key for key in dir(BaseConfig) if not ( @@ -616,7 +594,7 @@ def get_config(name: str) -> Any: # Override SQLAlchemy, allow both SQLAlchemy and plain Pydantic models def __init__( - cls, classname: str, bases: Tuple[type, ...], dict_: Dict[str, Any], **kw: Any + cls, classname: str, bases: tuple[type, ...], dict_: dict[str, Any], **kw: Any ) -> None: # Only one of the base classes (or the current one) should be a table model # this allows FastAPI cloning a SQLModel for the response_model without @@ -643,7 +621,7 @@ def __init__( relationship_to = get_relationship_to( name=rel_name, rel_info=rel_info, annotation=ann ) - rel_kwargs: Dict[str, Any] = {} + rel_kwargs: dict[str, Any] = {} if rel_info.back_populates: rel_kwargs["back_populates"] = rel_info.back_populates if rel_info.cascade_delete: @@ -659,7 +637,7 @@ def __init__( f"model {rel_info.link_model}" ) rel_kwargs["secondary"] = local_table - rel_args: List[Any] = [] + rel_args: list[Any] = [] if rel_info.sa_relationship_args: rel_args.extend(rel_info.sa_relationship_args) if rel_info.sa_relationship_kwargs: @@ -787,7 +765,7 @@ def get_column_from_field(field: Any) -> Column: # type: ignore args.extend(list(cast(Sequence[Any], sa_column_args))) sa_column_kwargs = getattr(field_info, "sa_column_kwargs", Undefined) if sa_column_kwargs is not Undefined: - kwargs.update(cast(Dict[Any, Any], sa_column_kwargs)) + kwargs.update(cast(dict[Any, Any], sa_column_kwargs)) return Column(sa_type, *args, **kwargs) # type: ignore @@ -802,7 +780,7 @@ class SQLModel(BaseModel, metaclass=SQLModelMetaclass, registry=default_registry # SQLAlchemy needs to set weakref(s), Pydantic will set the other slots values __slots__ = ("__weakref__",) __tablename__: ClassVar[Union[str, Callable[..., str]]] - __sqlmodel_relationships__: ClassVar[Dict[str, RelationshipProperty[Any]]] + __sqlmodel_relationships__: ClassVar[builtins.dict[str, RelationshipProperty[Any]]] __name__: ClassVar[str] metadata: ClassVar[MetaData] __allow_unmapped__ = True # https://docs.sqlalchemy.org/en/20/changelog/migration_20.html#migration-20-step-six @@ -857,7 +835,7 @@ def __setattr__(self, name: str, value: Any) -> None: if name not in self.__sqlmodel_relationships__: super().__setattr__(name, value) - def __repr_args__(self) -> Sequence[Tuple[Optional[str], Any]]: + def __repr_args__(self) -> Sequence[tuple[Optional[str], Any]]: # Don't show SQLAlchemy private attributes return [ (k, v) @@ -871,13 +849,13 @@ def __tablename__(cls) -> str: @classmethod def model_validate( # type: ignore[override] - cls: Type[_TSQLModel], + cls: type[_TSQLModel], obj: Any, *, strict: Union[bool, None] = None, from_attributes: Union[bool, None] = None, - context: Union[Dict[str, Any], None] = None, - update: Union[Dict[str, Any], None] = None, + context: Union[builtins.dict[str, Any], None] = None, + update: Union[builtins.dict[str, Any], None] = None, ) -> _TSQLModel: return sqlmodel_validate( cls=cls, @@ -904,10 +882,10 @@ def model_dump( warnings: Union[bool, Literal["none", "warn", "error"]] = True, fallback: Union[Callable[[Any], Any], None] = None, # v2.11 serialize_as_any: bool = False, # v2.7 - ) -> Dict[str, Any]: + ) -> builtins.dict[str, Any]: if PYDANTIC_MINOR_VERSION < (2, 11): by_alias = by_alias or False - extra_kwargs: Dict[str, Any] = {} + extra_kwargs: dict[str, Any] = {} if PYDANTIC_MINOR_VERSION >= (2, 7): extra_kwargs["context"] = context extra_kwargs["serialize_as_any"] = serialize_as_any @@ -953,7 +931,7 @@ def dict( exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, - ) -> Dict[str, Any]: + ) -> builtins.dict[str, Any]: return self.model_dump( include=include, exclude=exclude, @@ -971,7 +949,9 @@ def dict( """ ) def from_orm( - cls: Type[_TSQLModel], obj: Any, update: Optional[Dict[str, Any]] = None + cls: type[_TSQLModel], + obj: Any, + update: Optional[builtins.dict[str, Any]] = None, ) -> _TSQLModel: return cls.model_validate(obj, update=update) @@ -983,7 +963,9 @@ def from_orm( """ ) def parse_obj( - cls: Type[_TSQLModel], obj: Any, update: Optional[Dict[str, Any]] = None + cls: type[_TSQLModel], + obj: Any, + update: Optional[builtins.dict[str, Any]] = None, ) -> _TSQLModel: if not IS_PYDANTIC_V2: obj = cls._enforce_dict_if_root(obj) # type: ignore[attr-defined] # noqa @@ -1004,8 +986,8 @@ def _calculate_keys( include: Optional[Mapping[Union[int, str], Any]], exclude: Optional[Mapping[Union[int, str], Any]], exclude_unset: bool, - update: Optional[Dict[str, Any]] = None, - ) -> Optional[AbstractSet[str]]: + update: Optional[builtins.dict[str, Any]] = None, + ) -> Optional[Set[str]]: return _calculate_keys( self, include=include, @@ -1016,9 +998,9 @@ def _calculate_keys( def sqlmodel_update( self: _TSQLModel, - obj: Union[Dict[str, Any], BaseModel], + obj: Union[builtins.dict[str, Any], BaseModel], *, - update: Union[Dict[str, Any], None] = None, + update: Union[builtins.dict[str, Any], None] = None, ) -> _TSQLModel: use_update = (update or {}).copy() if isinstance(obj, dict): diff --git a/sqlmodel/orm/session.py b/sqlmodel/orm/session.py index 9e82d48a73..d6966c0e6d 100644 --- a/sqlmodel/orm/session.py +++ b/sqlmodel/orm/session.py @@ -1,9 +1,7 @@ +from collections.abc import Mapping, Sequence from typing import ( Any, - Dict, - Mapping, Optional, - Sequence, TypeVar, Union, overload, @@ -34,7 +32,7 @@ def exec( *, params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, execution_options: Mapping[str, Any] = util.EMPTY_DICT, - bind_arguments: Optional[Dict[str, Any]] = None, + bind_arguments: Optional[dict[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, ) -> TupleResult[_TSelectParam]: ... @@ -46,7 +44,7 @@ def exec( *, params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, execution_options: Mapping[str, Any] = util.EMPTY_DICT, - bind_arguments: Optional[Dict[str, Any]] = None, + bind_arguments: Optional[dict[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, ) -> ScalarResult[_TSelectParam]: ... @@ -58,7 +56,7 @@ def exec( *, params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, execution_options: Mapping[str, Any] = util.EMPTY_DICT, - bind_arguments: Optional[Dict[str, Any]] = None, + bind_arguments: Optional[dict[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, ) -> CursorResult[Any]: ... @@ -74,7 +72,7 @@ def exec( *, params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, execution_options: Mapping[str, Any] = util.EMPTY_DICT, - bind_arguments: Optional[Dict[str, Any]] = None, + bind_arguments: Optional[dict[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, ) -> Union[ @@ -119,7 +117,7 @@ def execute( params: Optional[_CoreAnyExecuteParams] = None, *, execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT, - bind_arguments: Optional[Dict[str, Any]] = None, + bind_arguments: Optional[dict[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, ) -> Result[Any]: diff --git a/sqlmodel/sql/_expression_select_cls.py b/sqlmodel/sql/_expression_select_cls.py index 9fd8609956..8f5469f4f2 100644 --- a/sqlmodel/sql/_expression_select_cls.py +++ b/sqlmodel/sql/_expression_select_cls.py @@ -1,5 +1,4 @@ from typing import ( - Tuple, TypeVar, Union, ) @@ -15,7 +14,7 @@ # Separate this class in SelectBase, Select, and SelectOfScalar so that they can share # where and having without having type overlap incompatibility in session.exec(). -class SelectBase(_Select[Tuple[_T]]): +class SelectBase(_Select[tuple[_T]]): inherit_cache = True def where(self, *whereclause: Union[_ColumnExpressionArgument[bool], bool]) -> Self: diff --git a/sqlmodel/sql/_expression_select_gen.py b/sqlmodel/sql/_expression_select_gen.py index 08aa59ad61..f602732fb3 100644 --- a/sqlmodel/sql/_expression_select_gen.py +++ b/sqlmodel/sql/_expression_select_gen.py @@ -1,12 +1,9 @@ # WARNING: do not modify this code, it is generated by _expression_select_gen.py.jinja2 +from collections.abc import Mapping, Sequence from datetime import datetime from typing import ( Any, - Mapping, - Sequence, - Tuple, - Type, TypeVar, Union, overload, @@ -29,7 +26,7 @@ _TCCA = Union[ TypedColumnsClauseRole[_T], SQLCoreOperations[_T], - Type[_T], + type[_T], ] # Generated TypeVars start @@ -126,28 +123,28 @@ def select(__ent0: _TScalar_0) -> SelectOfScalar[_TScalar_0]: # type: ignore def select( # type: ignore __ent0: _TCCA[_T0], __ent1: _TCCA[_T1], -) -> Select[Tuple[_T0, _T1]]: ... +) -> Select[tuple[_T0, _T1]]: ... @overload def select( # type: ignore __ent0: _TCCA[_T0], entity_1: _TScalar_1, -) -> Select[Tuple[_T0, _TScalar_1]]: ... +) -> Select[tuple[_T0, _TScalar_1]]: ... @overload def select( # type: ignore entity_0: _TScalar_0, __ent1: _TCCA[_T1], -) -> Select[Tuple[_TScalar_0, _T1]]: ... +) -> Select[tuple[_TScalar_0, _T1]]: ... @overload def select( # type: ignore entity_0: _TScalar_0, entity_1: _TScalar_1, -) -> Select[Tuple[_TScalar_0, _TScalar_1]]: ... +) -> Select[tuple[_TScalar_0, _TScalar_1]]: ... @overload @@ -155,7 +152,7 @@ def select( # type: ignore __ent0: _TCCA[_T0], __ent1: _TCCA[_T1], __ent2: _TCCA[_T2], -) -> Select[Tuple[_T0, _T1, _T2]]: ... +) -> Select[tuple[_T0, _T1, _T2]]: ... @overload @@ -163,7 +160,7 @@ def select( # type: ignore __ent0: _TCCA[_T0], __ent1: _TCCA[_T1], entity_2: _TScalar_2, -) -> Select[Tuple[_T0, _T1, _TScalar_2]]: ... +) -> Select[tuple[_T0, _T1, _TScalar_2]]: ... @overload @@ -171,7 +168,7 @@ def select( # type: ignore __ent0: _TCCA[_T0], entity_1: _TScalar_1, __ent2: _TCCA[_T2], -) -> Select[Tuple[_T0, _TScalar_1, _T2]]: ... +) -> Select[tuple[_T0, _TScalar_1, _T2]]: ... @overload @@ -179,7 +176,7 @@ def select( # type: ignore __ent0: _TCCA[_T0], entity_1: _TScalar_1, entity_2: _TScalar_2, -) -> Select[Tuple[_T0, _TScalar_1, _TScalar_2]]: ... +) -> Select[tuple[_T0, _TScalar_1, _TScalar_2]]: ... @overload @@ -187,7 +184,7 @@ def select( # type: ignore entity_0: _TScalar_0, __ent1: _TCCA[_T1], __ent2: _TCCA[_T2], -) -> Select[Tuple[_TScalar_0, _T1, _T2]]: ... +) -> Select[tuple[_TScalar_0, _T1, _T2]]: ... @overload @@ -195,7 +192,7 @@ def select( # type: ignore entity_0: _TScalar_0, __ent1: _TCCA[_T1], entity_2: _TScalar_2, -) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2]]: ... +) -> Select[tuple[_TScalar_0, _T1, _TScalar_2]]: ... @overload @@ -203,7 +200,7 @@ def select( # type: ignore entity_0: _TScalar_0, entity_1: _TScalar_1, __ent2: _TCCA[_T2], -) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2]]: ... +) -> Select[tuple[_TScalar_0, _TScalar_1, _T2]]: ... @overload @@ -211,7 +208,7 @@ def select( # type: ignore entity_0: _TScalar_0, entity_1: _TScalar_1, entity_2: _TScalar_2, -) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2]]: ... +) -> Select[tuple[_TScalar_0, _TScalar_1, _TScalar_2]]: ... @overload @@ -220,7 +217,7 @@ def select( # type: ignore __ent1: _TCCA[_T1], __ent2: _TCCA[_T2], __ent3: _TCCA[_T3], -) -> Select[Tuple[_T0, _T1, _T2, _T3]]: ... +) -> Select[tuple[_T0, _T1, _T2, _T3]]: ... @overload @@ -229,7 +226,7 @@ def select( # type: ignore __ent1: _TCCA[_T1], __ent2: _TCCA[_T2], entity_3: _TScalar_3, -) -> Select[Tuple[_T0, _T1, _T2, _TScalar_3]]: ... +) -> Select[tuple[_T0, _T1, _T2, _TScalar_3]]: ... @overload @@ -238,7 +235,7 @@ def select( # type: ignore __ent1: _TCCA[_T1], entity_2: _TScalar_2, __ent3: _TCCA[_T3], -) -> Select[Tuple[_T0, _T1, _TScalar_2, _T3]]: ... +) -> Select[tuple[_T0, _T1, _TScalar_2, _T3]]: ... @overload @@ -247,7 +244,7 @@ def select( # type: ignore __ent1: _TCCA[_T1], entity_2: _TScalar_2, entity_3: _TScalar_3, -) -> Select[Tuple[_T0, _T1, _TScalar_2, _TScalar_3]]: ... +) -> Select[tuple[_T0, _T1, _TScalar_2, _TScalar_3]]: ... @overload @@ -256,7 +253,7 @@ def select( # type: ignore entity_1: _TScalar_1, __ent2: _TCCA[_T2], __ent3: _TCCA[_T3], -) -> Select[Tuple[_T0, _TScalar_1, _T2, _T3]]: ... +) -> Select[tuple[_T0, _TScalar_1, _T2, _T3]]: ... @overload @@ -265,7 +262,7 @@ def select( # type: ignore entity_1: _TScalar_1, __ent2: _TCCA[_T2], entity_3: _TScalar_3, -) -> Select[Tuple[_T0, _TScalar_1, _T2, _TScalar_3]]: ... +) -> Select[tuple[_T0, _TScalar_1, _T2, _TScalar_3]]: ... @overload @@ -274,7 +271,7 @@ def select( # type: ignore entity_1: _TScalar_1, entity_2: _TScalar_2, __ent3: _TCCA[_T3], -) -> Select[Tuple[_T0, _TScalar_1, _TScalar_2, _T3]]: ... +) -> Select[tuple[_T0, _TScalar_1, _TScalar_2, _T3]]: ... @overload @@ -283,7 +280,7 @@ def select( # type: ignore entity_1: _TScalar_1, entity_2: _TScalar_2, entity_3: _TScalar_3, -) -> Select[Tuple[_T0, _TScalar_1, _TScalar_2, _TScalar_3]]: ... +) -> Select[tuple[_T0, _TScalar_1, _TScalar_2, _TScalar_3]]: ... @overload @@ -292,7 +289,7 @@ def select( # type: ignore __ent1: _TCCA[_T1], __ent2: _TCCA[_T2], __ent3: _TCCA[_T3], -) -> Select[Tuple[_TScalar_0, _T1, _T2, _T3]]: ... +) -> Select[tuple[_TScalar_0, _T1, _T2, _T3]]: ... @overload @@ -301,7 +298,7 @@ def select( # type: ignore __ent1: _TCCA[_T1], __ent2: _TCCA[_T2], entity_3: _TScalar_3, -) -> Select[Tuple[_TScalar_0, _T1, _T2, _TScalar_3]]: ... +) -> Select[tuple[_TScalar_0, _T1, _T2, _TScalar_3]]: ... @overload @@ -310,7 +307,7 @@ def select( # type: ignore __ent1: _TCCA[_T1], entity_2: _TScalar_2, __ent3: _TCCA[_T3], -) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2, _T3]]: ... +) -> Select[tuple[_TScalar_0, _T1, _TScalar_2, _T3]]: ... @overload @@ -319,7 +316,7 @@ def select( # type: ignore __ent1: _TCCA[_T1], entity_2: _TScalar_2, entity_3: _TScalar_3, -) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2, _TScalar_3]]: ... +) -> Select[tuple[_TScalar_0, _T1, _TScalar_2, _TScalar_3]]: ... @overload @@ -328,7 +325,7 @@ def select( # type: ignore entity_1: _TScalar_1, __ent2: _TCCA[_T2], __ent3: _TCCA[_T3], -) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2, _T3]]: ... +) -> Select[tuple[_TScalar_0, _TScalar_1, _T2, _T3]]: ... @overload @@ -337,7 +334,7 @@ def select( # type: ignore entity_1: _TScalar_1, __ent2: _TCCA[_T2], entity_3: _TScalar_3, -) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2, _TScalar_3]]: ... +) -> Select[tuple[_TScalar_0, _TScalar_1, _T2, _TScalar_3]]: ... @overload @@ -346,7 +343,7 @@ def select( # type: ignore entity_1: _TScalar_1, entity_2: _TScalar_2, __ent3: _TCCA[_T3], -) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2, _T3]]: ... +) -> Select[tuple[_TScalar_0, _TScalar_1, _TScalar_2, _T3]]: ... @overload @@ -355,7 +352,7 @@ def select( # type: ignore entity_1: _TScalar_1, entity_2: _TScalar_2, entity_3: _TScalar_3, -) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2, _TScalar_3]]: ... +) -> Select[tuple[_TScalar_0, _TScalar_1, _TScalar_2, _TScalar_3]]: ... # Generated overloads end diff --git a/sqlmodel/sql/_expression_select_gen.py.jinja2 b/sqlmodel/sql/_expression_select_gen.py.jinja2 index ef838e4168..ad0b59aef7 100644 --- a/sqlmodel/sql/_expression_select_gen.py.jinja2 +++ b/sqlmodel/sql/_expression_select_gen.py.jinja2 @@ -1,10 +1,7 @@ +from collections.abc import Mapping, Sequence from datetime import datetime from typing import ( Any, - Mapping, - Sequence, - Tuple, - Type, TypeVar, Union, overload, @@ -27,7 +24,7 @@ _T = TypeVar("_T") _TCCA = Union[ TypedColumnsClauseRole[_T], SQLCoreOperations[_T], - Type[_T], + type[_T], ] # Generated TypeVars start @@ -71,7 +68,7 @@ def select(__ent0: _TScalar_0) -> SelectOfScalar[_TScalar_0]: # type: ignore @overload def select( # type: ignore {% for arg in signature[0] %}{{ arg.name }}: {{ arg.annotation }}, {% endfor %} - ) -> Select[Tuple[{%for ret in signature[1] %}{{ ret }} {% if not loop.last %}, {% endif %}{% endfor %}]]: ... + ) -> Select[tuple[{%for ret in signature[1] %}{{ ret }} {% if not loop.last %}, {% endif %}{% endfor %}]]: ... {% endfor %} diff --git a/sqlmodel/sql/expression.py b/sqlmodel/sql/expression.py index f431747670..7732336118 100644 --- a/sqlmodel/sql/expression.py +++ b/sqlmodel/sql/expression.py @@ -1,11 +1,7 @@ +from collections.abc import Iterable, Mapping, Sequence from typing import ( Any, - Iterable, - Mapping, Optional, - Sequence, - Tuple, - Type, TypeVar, Union, ) @@ -46,7 +42,7 @@ _T = TypeVar("_T") -_TypeEngineArgument = Union[Type[TypeEngine[_T]], TypeEngine[_T]] +_TypeEngineArgument = Union[type[TypeEngine[_T]], TypeEngine[_T]] # Redefine operatos that would only take a column expresion to also take the (virtual) # types of Pydantic models, e.g. str instead of only Mapped[str]. @@ -94,7 +90,7 @@ def not_(clause: Union[_ColumnExpressionArgument[_T], _T]) -> ColumnElement[_T]: def case( *whens: Union[ - Tuple[Union[_ColumnExpressionArgument[bool], bool], Any], Mapping[Any, Any] + tuple[Union[_ColumnExpressionArgument[bool], bool], Any], Mapping[Any, Any] ], value: Optional[Any] = None, else_: Optional[Any] = None, @@ -181,8 +177,8 @@ def over( Any, ] ] = None, - range_: Optional[Tuple[Optional[int], Optional[int]]] = None, - rows: Optional[Tuple[Optional[int], Optional[int]]] = None, + range_: Optional[tuple[Optional[int], Optional[int]]] = None, + rows: Optional[tuple[Optional[int], Optional[int]]] = None, ) -> Over[_T]: return sqlalchemy.over( element, partition_by=partition_by, order_by=order_by, range_=range_, rows=rows @@ -192,7 +188,7 @@ def over( def tuple_( *clauses: Union[_ColumnExpressionArgument[Any], Any], types: Optional[Sequence["_TypeEngineArgument[Any]"]] = None, -) -> Tuple[Any, ...]: +) -> tuple[Any, ...]: return sqlalchemy.tuple_(*clauses, types=types) # type: ignore[return-value] diff --git a/tests/conftest.py b/tests/conftest.py index d8da629db0..39597095b0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,10 @@ import shutil import subprocess import sys +from collections.abc import Generator from dataclasses import dataclass, field from pathlib import Path -from typing import Any, Callable, Dict, Generator, List, Union +from typing import Any, Callable, Union from unittest.mock import patch import pytest @@ -53,10 +54,10 @@ def coverage_run(*, module: str, cwd: Union[str, Path]) -> subprocess.CompletedP def get_testing_print_function( - calls: List[List[Union[str, Dict[str, Any]]]], + calls: list[list[Union[str, dict[str, Any]]]], ) -> Callable[..., Any]: def new_print(*args: Any) -> None: - data: List[Any] = [] + data: list[Any] = [] for arg in args: if isinstance(arg, BaseModel): data.append(arg.model_dump()) @@ -75,7 +76,7 @@ def new_print(*args: Any) -> None: @dataclass class PrintMock: - calls: List[Any] = field(default_factory=list) + calls: list[Any] = field(default_factory=list) @pytest.fixture(name="print_mock") diff --git a/tests/test_aliases.py b/tests/test_aliases.py index 2ac9a5acd7..cbd5030c73 100644 --- a/tests/test_aliases.py +++ b/tests/test_aliases.py @@ -1,4 +1,4 @@ -from typing import Type, Union +from typing import Union import pytest from pydantic import BaseModel, ValidationError @@ -42,14 +42,14 @@ class Config: @pytest.mark.parametrize("model", [PydanticUser, SQLModelUser]) -def test_create_with_field_name(model: Union[Type[PydanticUser], Type[SQLModelUser]]): +def test_create_with_field_name(model: Union[type[PydanticUser], type[SQLModelUser]]): with pytest.raises(ValidationError): model(full_name="Alice") @pytest.mark.parametrize("model", [PydanticUserWithConfig, SQLModelUserWithConfig]) def test_create_with_field_name_with_config( - model: Union[Type[PydanticUserWithConfig], Type[SQLModelUserWithConfig]], + model: Union[type[PydanticUserWithConfig], type[SQLModelUserWithConfig]], ): user = model(full_name="Alice") assert user.full_name == "Alice" @@ -61,10 +61,10 @@ def test_create_with_field_name_with_config( ) def test_create_with_alias( model: Union[ - Type[PydanticUser], - Type[SQLModelUser], - Type[PydanticUserWithConfig], - Type[SQLModelUserWithConfig], + type[PydanticUser], + type[SQLModelUser], + type[PydanticUserWithConfig], + type[SQLModelUserWithConfig], ], ): user = model(fullName="Bob") # using alias @@ -73,7 +73,7 @@ def test_create_with_alias( @pytest.mark.parametrize("model", [PydanticUserWithConfig, SQLModelUserWithConfig]) def test_create_with_both_prefers_alias( - model: Union[Type[PydanticUserWithConfig], Type[SQLModelUserWithConfig]], + model: Union[type[PydanticUserWithConfig], type[SQLModelUserWithConfig]], ): user = model(full_name="IGNORED", fullName="Charlie") assert user.full_name == "Charlie" # alias should take precedence @@ -81,7 +81,7 @@ def test_create_with_both_prefers_alias( @pytest.mark.parametrize("model", [PydanticUser, SQLModelUser]) def test_dict_default_uses_field_names( - model: Union[Type[PydanticUser], Type[SQLModelUser]], + model: Union[type[PydanticUser], type[SQLModelUser]], ): user = model(fullName="Dana") if IS_PYDANTIC_V2 or isinstance(user, SQLModel): @@ -95,7 +95,7 @@ def test_dict_default_uses_field_names( @pytest.mark.parametrize("model", [PydanticUser, SQLModelUser]) def test_dict_by_alias_uses_aliases( - model: Union[Type[PydanticUser], Type[SQLModelUser]], + model: Union[type[PydanticUser], type[SQLModelUser]], ): user = model(fullName="Dana") if IS_PYDANTIC_V2 or isinstance(user, SQLModel): @@ -109,7 +109,7 @@ def test_dict_by_alias_uses_aliases( @pytest.mark.parametrize("model", [PydanticUser, SQLModelUser]) def test_json_by_alias( - model: Union[Type[PydanticUser], Type[SQLModelUser]], + model: Union[type[PydanticUser], type[SQLModelUser]], ): user = model(fullName="Frank") if IS_PYDANTIC_V2: @@ -156,7 +156,7 @@ def test_serialization_alias_runtimeerror_pydantic_v1(): @needs_pydanticv2 @pytest.mark.parametrize("model", [PydanticUserV2, SQLModelUserV2]) def test_create_with_validation_alias( - model: Union[Type[PydanticUserV2], Type[SQLModelUserV2]], + model: Union[type[PydanticUserV2], type[SQLModelUserV2]], ): user = model(firstName="John") assert user.first_name == "John" @@ -165,7 +165,7 @@ def test_create_with_validation_alias( @needs_pydanticv2 @pytest.mark.parametrize("model", [PydanticUserV2, SQLModelUserV2]) def test_serialize_with_serialization_alias( - model: Union[Type[PydanticUserV2], Type[SQLModelUserV2]], + model: Union[type[PydanticUserV2], type[SQLModelUserV2]], ): user = model(firstName="Jane") data = user.model_dump(by_alias=True) diff --git a/tests/test_field_sa_relationship.py b/tests/test_field_sa_relationship.py index 022a100a78..c3f8030299 100644 --- a/tests/test_field_sa_relationship.py +++ b/tests/test_field_sa_relationship.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional import pytest from sqlalchemy.orm import relationship @@ -13,7 +13,7 @@ class Team(SQLModel, table=True): name: str = Field(index=True) headquarters: str - heroes: List["Hero"] = Relationship( + heroes: list["Hero"] = Relationship( back_populates="team", sa_relationship_args=["Hero"], sa_relationship=relationship("Hero", back_populates="team"), @@ -37,7 +37,7 @@ class Team(SQLModel, table=True): name: str = Field(index=True) headquarters: str - heroes: List["Hero"] = Relationship( + heroes: list["Hero"] = Relationship( back_populates="team", sa_relationship_kwargs={"lazy": "selectin"}, sa_relationship=relationship("Hero", back_populates="team"), diff --git a/tests/test_main.py b/tests/test_main.py index 60d5c40ebb..c1508d181f 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional import pytest from sqlalchemy.exc import IntegrityError @@ -99,7 +99,7 @@ def test_sa_relationship_property(clear_sqlmodel): class Team(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) name: str = Field(unique=True) - heroes: List["Hero"] = Relationship( # noqa: F821 + heroes: list["Hero"] = Relationship( # noqa: F821 sa_relationship=RelationshipProperty("Hero", back_populates="team") ) diff --git a/tests/test_ondelete_raises.py b/tests/test_ondelete_raises.py index cbcab4ca41..761b8799a4 100644 --- a/tests/test_ondelete_raises.py +++ b/tests/test_ondelete_raises.py @@ -1,4 +1,4 @@ -from typing import Any, List, Union +from typing import Any, Union import pytest from sqlmodel import Field, Relationship, SQLModel @@ -10,7 +10,7 @@ def test_ondelete_requires_nullable(clear_sqlmodel: Any) -> None: class Team(SQLModel, table=True): id: Union[int, None] = Field(default=None, primary_key=True) - heroes: List["Hero"] = Relationship( + heroes: list["Hero"] = Relationship( back_populates="team", passive_deletes="all" ) diff --git a/tests/test_sqlalchemy_type_errors.py b/tests/test_sqlalchemy_type_errors.py index e211c46a34..cc3326b5ff 100644 --- a/tests/test_sqlalchemy_type_errors.py +++ b/tests/test_sqlalchemy_type_errors.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Optional, Union +from typing import Any, Optional, Union import pytest from sqlmodel import Field, SQLModel @@ -9,7 +9,7 @@ def test_type_list_breaks() -> None: class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - tags: List[str] + tags: list[str] def test_type_dict_breaks() -> None: @@ -17,7 +17,7 @@ def test_type_dict_breaks() -> None: class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - tags: Dict[str, Any] + tags: dict[str, Any] def test_type_union_breaks() -> None: diff --git a/tests/test_tutorial/test_automatic_id_none_refresh/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_automatic_id_none_refresh/test_tutorial001_tutorial002.py index b2c452f907..cd285ae438 100644 --- a/tests/test_tutorial/test_automatic_id_none_refresh/test_tutorial001_tutorial002.py +++ b/tests/test_tutorial/test_automatic_id_none_refresh/test_tutorial001_tutorial002.py @@ -1,6 +1,6 @@ import importlib from types import ModuleType -from typing import Any, Dict, List, Union +from typing import Any, Union import pytest from sqlmodel import create_engine @@ -8,7 +8,7 @@ from tests.conftest import PrintMock, needs_py310 -def check_calls(calls: List[List[Union[str, Dict[str, Any]]]]) -> None: +def check_calls(calls: list[list[Union[str, dict[str, Any]]]]) -> None: assert calls[0] == ["Before interacting with the database"] assert calls[1] == [ "Hero 1:", diff --git a/tests/test_tutorial/test_select/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_select/test_tutorial001_tutorial002.py index 3dc5c186c0..4d28303d7b 100644 --- a/tests/test_tutorial/test_select/test_tutorial001_tutorial002.py +++ b/tests/test_tutorial/test_select/test_tutorial001_tutorial002.py @@ -1,6 +1,6 @@ import importlib from types import ModuleType -from typing import Any, Dict, List, Union +from typing import Any, Union import pytest from sqlmodel import create_engine @@ -8,7 +8,7 @@ from ...conftest import PrintMock, needs_py310 -def check_calls(calls: List[List[Union[str, Dict[str, Any]]]]): +def check_calls(calls: list[list[Union[str, dict[str, Any]]]]): assert calls[0][0] == { "name": "Deadpond", "secret_name": "Dive Wilson", diff --git a/tests/test_tutorial/test_select/test_tutorial003_tutorial004.py b/tests/test_tutorial/test_select/test_tutorial003_tutorial004.py index e64d5f8b79..e8e3daf190 100644 --- a/tests/test_tutorial/test_select/test_tutorial003_tutorial004.py +++ b/tests/test_tutorial/test_select/test_tutorial003_tutorial004.py @@ -1,6 +1,6 @@ import importlib from types import ModuleType -from typing import Any, Dict, List, Union +from typing import Any, Union import pytest from sqlmodel import create_engine @@ -8,7 +8,7 @@ from ...conftest import PrintMock, needs_py310 -def check_calls(calls: List[List[Union[str, Dict[str, Any]]]]): +def check_calls(calls: list[list[Union[str, dict[str, Any]]]]): assert calls[0][0] == [ { "name": "Deadpond", From 7c7c66d0d7a4f1e718c33824a8e4798e77262040 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Dec 2025 11:23:18 +0000 Subject: [PATCH 886/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index fadb7658a8..157277089a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Breaking Changes + +* ➖ Drop support for Python 3.8. PR [#1696](https://github.com/fastapi/sqlmodel/pull/1696) by [@tiangolo](https://github.com/tiangolo). + ### Docs * ➖ Drop support for Python 3.8 in CI and docs. PR [#1695](https://github.com/fastapi/sqlmodel/pull/1695) by [@tiangolo](https://github.com/tiangolo). From 1d215b709869dc774bf43d034925ae700ca506a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 26 Dec 2025 03:42:29 -0800 Subject: [PATCH 887/906] =?UTF-8?q?=F0=9F=94=A7=20Update=20pre-commit,=20g?= =?UTF-8?q?enerate=20select=20on=20pre-commit,=20use=20local=20Ruff=20(#16?= =?UTF-8?q?97)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 20a49e7a94..41c64c030c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,20 +1,35 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: -- repo: https://github.com/pre-commit/pre-commit-hooks + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - - id: check-added-large-files - - id: check-toml - - id: check-yaml + - id: check-added-large-files + - id: check-toml + - id: check-yaml args: - - --unsafe - - id: end-of-file-fixer - - id: trailing-whitespace -- repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.10 + - --unsafe + - id: end-of-file-fixer + - id: trailing-whitespace + + - repo: local hooks: - - id: ruff - args: - - --fix - - id: ruff-format + - id: local-ruff-check + name: ruff check + entry: uv run ruff check --force-exclude --fix --exit-non-zero-on-fix + require_serial: true + language: unsupported + types: [python] + + - id: local-ruff-format + name: ruff format + entry: uv run ruff format --force-exclude --exit-non-zero-on-format + require_serial: true + language: unsupported + types: [python] + + - id: generate-select + language: unsupported + name: generate-select + entry: uv run ./scripts/generate_select.py + files: ^scripts/generate_select\.py|sqlmodel/sql/_expression_select_gen\.py\.jinja2$ From 328799c3d193d04fd8361a912a1263eec461f503 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Dec 2025 11:42:47 +0000 Subject: [PATCH 888/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 157277089a..aa907b69d3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -12,6 +12,7 @@ ### Internal +* 🔧 Update pre-commit, generate select on pre-commit, use local Ruff. PR [#1697](https://github.com/fastapi/sqlmodel/pull/1697) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump actions/checkout from 5 to 6. PR [#1692](https://github.com/fastapi/sqlmodel/pull/1692) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Add pre-commit workflow. PR [#1684](https://github.com/fastapi/sqlmodel/pull/1684) by [@YuriiMotov](https://github.com/YuriiMotov). * ✅ Simplify tests for code examples, one test file for multiple variants. PR [#1664](https://github.com/fastapi/sqlmodel/pull/1664) by [@YuriiMotov](https://github.com/YuriiMotov). From 9c13e0012bd34475d9134e76b043fcbdf00d8de0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 26 Dec 2025 12:43:53 +0100 Subject: [PATCH 889/906] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.0.?= =?UTF-8?q?30?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index 00499bba3e..ad21f1c666 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.0.29" +__version__ = "0.0.30" # Re-export from SQLAlchemy from sqlalchemy.engine import create_engine as create_engine From 98d61b74127a765027837d427ff1ff72a6b92fbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 26 Dec 2025 12:44:20 +0100 Subject: [PATCH 890/906] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.0.?= =?UTF-8?q?30?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index aa907b69d3..1cfd99f8ac 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest Changes +## 0.0.30 + ### Breaking Changes * ➖ Drop support for Python 3.8. PR [#1696](https://github.com/fastapi/sqlmodel/pull/1696) by [@tiangolo](https://github.com/tiangolo). From 29225842db1e999ef331f6da44bd8f54d2119f59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 26 Dec 2025 13:02:52 +0100 Subject: [PATCH 891/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 1cfd99f8ac..4e065d14b8 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,7 +10,7 @@ ### Docs -* ➖ Drop support for Python 3.8 in CI and docs. PR [#1695](https://github.com/fastapi/sqlmodel/pull/1695) by [@tiangolo](https://github.com/tiangolo). +* ➖ Drop support for Python 3.8 in CI and docs. PR [#1695](https://github.com/fastapi/sqlmodel/pull/1695) by [@YuriiMotov](https://github.com/YuriiMotov) and [@tiangolo](https://github.com/tiangolo). ### Internal From 8695a273ce0cd368feadedbaafbc77e11a3d81ea Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Fri, 26 Dec 2025 15:55:21 +0100 Subject: [PATCH 892/906] =?UTF-8?q?=F0=9F=93=8C=20Relax=20`prek`=20version?= =?UTF-8?q?=20pin=20to=20`>=3D0.2.24,<1.0.0`=20(#1698)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Relax `prek` to `<1.0.0` --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 01da92143d..63212c3507 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,4 @@ -r requirements-tests.txt -r requirements-docs.txt -prek==0.2.24 +prek>=0.2.24,<1.0.0 From d2213cb65c273661749defe8bba7600dc1e69752 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Dec 2025 14:55:37 +0000 Subject: [PATCH 893/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 4e065d14b8..01251e61e6 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Internal + +* 📌 Relax `prek` version pin to `>=0.2.24,<1.0.0`. PR [#1698](https://github.com/fastapi/sqlmodel/pull/1698) by [@YuriiMotov](https://github.com/YuriiMotov). + ## 0.0.30 ### Breaking Changes From fbb2fca7bee2b2039d80feaecf6ab5fae49269a5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 Dec 2025 16:40:26 +0100 Subject: [PATCH 894/906] =?UTF-8?q?=E2=AC=86=20Bump=20typer=20from=200.20.?= =?UTF-8?q?1=20to=200.21.0=20(#1694)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [typer](https://github.com/fastapi/typer) from 0.20.1 to 0.21.0. - [Release notes](https://github.com/fastapi/typer/releases) - [Changelog](https://github.com/fastapi/typer/blob/master/docs/release-notes.md) - [Commits](https://github.com/fastapi/typer/compare/0.20.1...0.21.0) --- updated-dependencies: - dependency-name: typer dependency-version: 0.21.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index e4978793e0..a7c56cd294 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -14,6 +14,6 @@ mkdocstrings[python]==0.30.1 griffe-typingdoc==0.3.0 griffe-warnings-deprecated==1.1.0 # For griffe, it formats with black -typer == 0.20.1 +typer == 0.21.0 mkdocs-macros-plugin==1.5.0 markdown-include-variants==0.0.8 From 4e7cfac94eeddd4c9d404ee0b8d03c6c784db43a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Dec 2025 15:40:45 +0000 Subject: [PATCH 895/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 01251e61e6..d69939758d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Internal +* ⬆ Bump typer from 0.20.1 to 0.21.0. PR [#1694](https://github.com/fastapi/sqlmodel/pull/1694) by [@dependabot[bot]](https://github.com/apps/dependabot). * 📌 Relax `prek` version pin to `>=0.2.24,<1.0.0`. PR [#1698](https://github.com/fastapi/sqlmodel/pull/1698) by [@YuriiMotov](https://github.com/YuriiMotov). ## 0.0.30 From 547344d8e9462ab0057cc5f8de9f30c41d2e3dae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 Dec 2025 16:40:56 +0100 Subject: [PATCH 896/906] =?UTF-8?q?=E2=AC=86=20Bump=20mkdocs-material=20fr?= =?UTF-8?q?om=209.7.0=20to=209.7.1=20(#1690)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.7.0 to 9.7.1. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.7.0...9.7.1) --- updated-dependencies: - dependency-name: mkdocs-material dependency-version: 9.7.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index a7c56cd294..b35f75d894 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,6 +1,6 @@ -e . -r requirements-docs-tests.txt -mkdocs-material==9.7.0 +mkdocs-material==9.7.1 mdx-include >=1.4.1,<2.0.0 mkdocs-redirects>=1.2.1,<1.3.0 pyyaml >=5.3.1,<7.0.0 From 8ab5d68982f17ab537b6857754cc33ae9367d5cc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Dec 2025 15:41:32 +0000 Subject: [PATCH 897/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index d69939758d..6a4dbe026c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Internal +* ⬆ Bump mkdocs-material from 9.7.0 to 9.7.1. PR [#1690](https://github.com/fastapi/sqlmodel/pull/1690) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump typer from 0.20.1 to 0.21.0. PR [#1694](https://github.com/fastapi/sqlmodel/pull/1694) by [@dependabot[bot]](https://github.com/apps/dependabot). * 📌 Relax `prek` version pin to `>=0.2.24,<1.0.0`. PR [#1698](https://github.com/fastapi/sqlmodel/pull/1698) by [@YuriiMotov](https://github.com/YuriiMotov). From b9241afcd4a79c268d5ba76b47fdce22fa47e8a2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 Dec 2025 16:48:24 +0100 Subject: [PATCH 898/906] =?UTF-8?q?=E2=AC=86=20Bump=20dirty-equals=20from?= =?UTF-8?q?=200.9.0=20to=200.11=20(#1649)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [dirty-equals](https://github.com/samuelcolvin/dirty-equals) from 0.9.0 to 0.11. - [Release notes](https://github.com/samuelcolvin/dirty-equals/releases) - [Commits](https://github.com/samuelcolvin/dirty-equals/compare/v0.9.0...v0.11.0) --- updated-dependencies: - dependency-name: dirty-equals dependency-version: '0.11' dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: svlandeg Co-authored-by: Sofie Van Landeghem --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 378eaca669..82d1f8480c 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -7,6 +7,6 @@ ruff ==0.14.10 # For FastAPI tests fastapi >=0.103.2,<0.126.0 httpx ==0.28.1 -dirty-equals ==0.9.0 +dirty-equals ==0.11 jinja2 ==3.1.6 typing-extensions ==4.15.0 From 349bc7330a69baf8bdfd21eb2ed5fe9760c33111 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Dec 2025 15:48:41 +0000 Subject: [PATCH 899/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 6a4dbe026c..ad044a98a8 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Internal +* ⬆ Bump dirty-equals from 0.9.0 to 0.11. PR [#1649](https://github.com/fastapi/sqlmodel/pull/1649) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-material from 9.7.0 to 9.7.1. PR [#1690](https://github.com/fastapi/sqlmodel/pull/1690) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump typer from 0.20.1 to 0.21.0. PR [#1694](https://github.com/fastapi/sqlmodel/pull/1694) by [@dependabot[bot]](https://github.com/apps/dependabot). * 📌 Relax `prek` version pin to `>=0.2.24,<1.0.0`. PR [#1698](https://github.com/fastapi/sqlmodel/pull/1698) by [@YuriiMotov](https://github.com/YuriiMotov). From 5917b192f2ea06059e90172242c4457cdb126461 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 28 Dec 2025 04:32:26 -0800 Subject: [PATCH 900/906] =?UTF-8?q?=E2=9E=96=20Drop=20support=20for=20Pyda?= =?UTF-8?q?ntic=20v1=20(#1701)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] --- .github/workflows/test.yml | 14 - pyproject.toml | 2 +- sqlmodel/_compat.py | 773 ++++++++++++----------------------- sqlmodel/main.py | 130 ++---- tests/conftest.py | 4 - tests/test_aliases.py | 94 +---- tests/test_annotated_uuid.py | 3 - tests/test_enums.py | 45 -- tests/test_fields_set.py | 7 +- tests/test_validation.py | 32 -- uv.lock | 388 ++++++++++++++++++ 11 files changed, 696 insertions(+), 796 deletions(-) create mode 100644 uv.lock diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2fea4779a9..106731cde4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,27 +27,19 @@ jobs: matrix: os: [ ubuntu-latest, windows-latest, macos-latest ] python-version: [ "3.14" ] - pydantic-version: - - pydantic-v2 include: - os: windows-latest python-version: "3.9" - pydantic-version: pydantic-v2 - os: ubuntu-latest python-version: "3.10" - pydantic-version: pydantic-v1 - os: macos-latest python-version: "3.11" - pydantic-version: pydantic-v2 - os: windows-latest python-version: "3.12" - pydantic-version: pydantic-v1 - os: ubuntu-latest python-version: "3.13" - pydantic-version: pydantic-v1 - os: macos-latest python-version: "3.13" - pydantic-version: pydantic-v2 fail-fast: false runs-on: ${{ matrix.os }} steps: @@ -72,12 +64,6 @@ jobs: limit-access-to-actor: true - name: Install Dependencies run: uv pip install -r requirements-tests.txt - - name: Install Pydantic v1 - if: matrix.pydantic-version == 'pydantic-v1' - run: uv pip install --upgrade "pydantic>=1.10.0,<2.0.0" - - name: Install Pydantic v2 - if: matrix.pydantic-version == 'pydantic-v2' - run: uv pip install --upgrade "pydantic>=2.0.2,<3.0.0" - run: mkdir coverage - name: Test run: bash scripts/test.sh diff --git a/pyproject.toml b/pyproject.toml index 0a95bfab13..2bdbe67c15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ classifiers = [ dependencies = [ "SQLAlchemy >=2.0.14,<2.1.0", - "pydantic >=1.10.13,<3.0.0", + "pydantic>=2.7.0", ] [project.urls] diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index 60874149bf..5907d279c8 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -1,6 +1,6 @@ import sys import types -from collections.abc import Generator, Mapping, Set +from collections.abc import Generator from contextlib import contextmanager from contextvars import ContextVar from dataclasses import dataclass @@ -8,22 +8,27 @@ TYPE_CHECKING, Annotated, Any, - Callable, ForwardRef, Optional, TypeVar, Union, ) +from annotated_types import MaxLen from pydantic import VERSION as P_VERSION from pydantic import BaseModel +from pydantic import ConfigDict as ConfigDict +from pydantic._internal._fields import PydanticMetadata +from pydantic._internal._model_construction import ModelMetaclass as ModelMetaclass +from pydantic._internal._repr import Representation as Representation from pydantic.fields import FieldInfo +from pydantic_core import PydanticUndefined as Undefined +from pydantic_core import PydanticUndefinedType as PydanticUndefinedType from typing_extensions import get_args, get_origin -# Reassign variable to make it reexported for mypy -PYDANTIC_VERSION = P_VERSION +BaseConfig = ConfigDict +UndefinedType = PydanticUndefinedType PYDANTIC_MINOR_VERSION = tuple(int(i) for i in P_VERSION.split(".")[:2]) -IS_PYDANTIC_V2 = PYDANTIC_MINOR_VERSION[0] == 2 if TYPE_CHECKING: @@ -69,525 +74,271 @@ def partial_init() -> Generator[None, None, None]: finish_init.reset(token) -if IS_PYDANTIC_V2: - from annotated_types import MaxLen - from pydantic import ConfigDict as BaseConfig - from pydantic._internal._fields import PydanticMetadata - from pydantic._internal._model_construction import ModelMetaclass - from pydantic._internal._repr import Representation as Representation - from pydantic_core import PydanticUndefined as Undefined - from pydantic_core import PydanticUndefinedType as UndefinedType - - # Dummy for types, to make it importable - class ModelField: - pass - - class SQLModelConfig(BaseConfig, total=False): - table: Optional[bool] - registry: Optional[Any] - - def get_config_value( - *, model: InstanceOrType["SQLModel"], parameter: str, default: Any = None - ) -> Any: - return model.model_config.get(parameter, default) - - def set_config_value( - *, - model: InstanceOrType["SQLModel"], - parameter: str, - value: Any, - ) -> None: - model.model_config[parameter] = value # type: ignore[literal-required] - - def get_model_fields(model: InstanceOrType[BaseModel]) -> dict[str, "FieldInfo"]: - # TODO: refactor the usage of this function to always pass the class - # not the instance, and then remove this extra check - # this is for compatibility with Pydantic v3 - if isinstance(model, type): - use_model = model +class SQLModelConfig(BaseConfig, total=False): + table: Optional[bool] + registry: Optional[Any] + + +def get_model_fields(model: InstanceOrType[BaseModel]) -> dict[str, "FieldInfo"]: + # TODO: refactor the usage of this function to always pass the class + # not the instance, and then remove this extra check + # this is for compatibility with Pydantic v3 + if isinstance(model, type): + use_model = model + else: + use_model = model.__class__ + return use_model.model_fields + + +def init_pydantic_private_attrs(new_object: InstanceOrType["SQLModel"]) -> None: + object.__setattr__(new_object, "__pydantic_fields_set__", set()) + object.__setattr__(new_object, "__pydantic_extra__", None) + object.__setattr__(new_object, "__pydantic_private__", None) + + +def get_annotations(class_dict: dict[str, Any]) -> dict[str, Any]: + raw_annotations: dict[str, Any] = class_dict.get("__annotations__", {}) + if sys.version_info >= (3, 14) and "__annotations__" not in class_dict: + # See https://github.com/pydantic/pydantic/pull/11991 + from annotationlib import ( + Format, + call_annotate_function, + get_annotate_from_class_namespace, + ) + + if annotate := get_annotate_from_class_namespace(class_dict): + raw_annotations = call_annotate_function(annotate, format=Format.FORWARDREF) + return raw_annotations + + +def is_table_model_class(cls: type[Any]) -> bool: + config = getattr(cls, "model_config", {}) + if config: + return config.get("table", False) or False + return False + + +def get_relationship_to( + name: str, + rel_info: "RelationshipInfo", + annotation: Any, +) -> Any: + origin = get_origin(annotation) + use_annotation = annotation + # Direct relationships (e.g. 'Team' or Team) have None as an origin + if origin is None: + if isinstance(use_annotation, ForwardRef): + use_annotation = use_annotation.__forward_arg__ else: - use_model = model.__class__ - return use_model.model_fields - - def get_fields_set( - object: InstanceOrType["SQLModel"], - ) -> Union[set[str], Callable[[BaseModel], set[str]]]: - return object.model_fields_set - - def init_pydantic_private_attrs(new_object: InstanceOrType["SQLModel"]) -> None: - object.__setattr__(new_object, "__pydantic_fields_set__", set()) - object.__setattr__(new_object, "__pydantic_extra__", None) - object.__setattr__(new_object, "__pydantic_private__", None) - - def get_annotations(class_dict: dict[str, Any]) -> dict[str, Any]: - raw_annotations: dict[str, Any] = class_dict.get("__annotations__", {}) - if sys.version_info >= (3, 14) and "__annotations__" not in class_dict: - # See https://github.com/pydantic/pydantic/pull/11991 - from annotationlib import ( - Format, - call_annotate_function, - get_annotate_from_class_namespace, + return use_annotation + # If Union (e.g. Optional), get the real field + elif _is_union_type(origin): + use_annotation = get_args(annotation) + if len(use_annotation) > 2: + raise ValueError("Cannot have a (non-optional) union as a SQLAlchemy field") + arg1, arg2 = use_annotation + if arg1 is NoneType and arg2 is not NoneType: + use_annotation = arg2 + elif arg2 is NoneType and arg1 is not NoneType: + use_annotation = arg1 + else: + raise ValueError( + "Cannot have a Union of None and None as a SQLAlchemy field" ) - if annotate := get_annotate_from_class_namespace(class_dict): - raw_annotations = call_annotate_function( - annotate, format=Format.FORWARDREF - ) - return raw_annotations + # If a list, then also get the real field + elif origin is list: + use_annotation = get_args(annotation)[0] - def is_table_model_class(cls: type[Any]) -> bool: - config = getattr(cls, "model_config", {}) - if config: - return config.get("table", False) or False - return False + return get_relationship_to(name=name, rel_info=rel_info, annotation=use_annotation) - def get_relationship_to( - name: str, - rel_info: "RelationshipInfo", - annotation: Any, - ) -> Any: - origin = get_origin(annotation) - use_annotation = annotation - # Direct relationships (e.g. 'Team' or Team) have None as an origin - if origin is None: - if isinstance(use_annotation, ForwardRef): - use_annotation = use_annotation.__forward_arg__ - else: - return use_annotation - # If Union (e.g. Optional), get the real field - elif _is_union_type(origin): - use_annotation = get_args(annotation) - if len(use_annotation) > 2: - raise ValueError( - "Cannot have a (non-optional) union as a SQLAlchemy field" - ) - arg1, arg2 = use_annotation - if arg1 is NoneType and arg2 is not NoneType: - use_annotation = arg2 - elif arg2 is NoneType and arg1 is not NoneType: - use_annotation = arg1 - else: - raise ValueError( - "Cannot have a Union of None and None as a SQLAlchemy field" - ) - - # If a list, then also get the real field - elif origin is list: - use_annotation = get_args(annotation)[0] - - return get_relationship_to( - name=name, rel_info=rel_info, annotation=use_annotation - ) - def is_field_noneable(field: "FieldInfo") -> bool: - if getattr(field, "nullable", Undefined) is not Undefined: - return field.nullable # type: ignore - origin = get_origin(field.annotation) - if origin is not None and _is_union_type(origin): - args = get_args(field.annotation) - if any(arg is NoneType for arg in args): - return True - if not field.is_required(): - if field.default is Undefined: - return False - if field.annotation is None or field.annotation is NoneType: - return True +def is_field_noneable(field: "FieldInfo") -> bool: + if getattr(field, "nullable", Undefined) is not Undefined: + return field.nullable # type: ignore + origin = get_origin(field.annotation) + if origin is not None and _is_union_type(origin): + args = get_args(field.annotation) + if any(arg is NoneType for arg in args): + return True + if not field.is_required(): + if field.default is Undefined: return False + if field.annotation is None or field.annotation is NoneType: + return True return False - - def get_sa_type_from_type_annotation(annotation: Any) -> Any: - # Resolve Optional fields - if annotation is None: - raise ValueError("Missing field type") - origin = get_origin(annotation) - if origin is None: - return annotation - elif origin is Annotated: - return get_sa_type_from_type_annotation(get_args(annotation)[0]) - if _is_union_type(origin): - bases = get_args(annotation) - if len(bases) > 2: - raise ValueError( - "Cannot have a (non-optional) union as a SQLAlchemy field" - ) - # Non optional unions are not allowed - if bases[0] is not NoneType and bases[1] is not NoneType: - raise ValueError( - "Cannot have a (non-optional) union as a SQLAlchemy field" - ) - # Optional unions are allowed - use_type = bases[0] if bases[0] is not NoneType else bases[1] - return get_sa_type_from_type_annotation(use_type) - return origin - - def get_sa_type_from_field(field: Any) -> Any: - type_: Any = field.annotation - return get_sa_type_from_type_annotation(type_) - - def get_field_metadata(field: Any) -> Any: - for meta in field.metadata: - if isinstance(meta, (PydanticMetadata, MaxLen)): - return meta - return FakeMetadata() - - def post_init_field_info(field_info: FieldInfo) -> None: - return None - - # Dummy to make it importable - def _calculate_keys( - self: "SQLModel", - include: Optional[Mapping[Union[int, str], Any]], - exclude: Optional[Mapping[Union[int, str], Any]], - exclude_unset: bool, - update: Optional[dict[str, Any]] = None, - ) -> Optional[Set[str]]: # pragma: no cover - return None - - def sqlmodel_table_construct( - *, - self_instance: _TSQLModel, - values: dict[str, Any], - _fields_set: Union[set[str], None] = None, - ) -> _TSQLModel: - # Copy from Pydantic's BaseModel.construct() - # Ref: https://github.com/pydantic/pydantic/blob/v2.5.2/pydantic/main.py#L198 - # Modified to not include everything, only the model fields, and to - # set relationships - # SQLModel override to get class SQLAlchemy __dict__ attributes and - # set them back in after creating the object - # new_obj = cls.__new__(cls) - cls = type(self_instance) - old_dict = self_instance.__dict__.copy() - # End SQLModel override - - fields_values: dict[str, Any] = {} - defaults: dict[ - str, Any - ] = {} # keeping this separate from `fields_values` helps us compute `_fields_set` - for name, field in cls.model_fields.items(): - if field.alias and field.alias in values: - fields_values[name] = values.pop(field.alias) - elif name in values: - fields_values[name] = values.pop(name) - elif not field.is_required(): - defaults[name] = field.get_default(call_default_factory=True) - if _fields_set is None: - _fields_set = set(fields_values.keys()) - fields_values.update(defaults) - - _extra: Union[dict[str, Any], None] = None - if cls.model_config.get("extra") == "allow": - _extra = {} - for k, v in values.items(): - _extra[k] = v - # SQLModel override, do not include everything, only the model fields - # else: - # fields_values.update(values) - # End SQLModel override - # SQLModel override + return False + + +def get_sa_type_from_type_annotation(annotation: Any) -> Any: + # Resolve Optional fields + if annotation is None: + raise ValueError("Missing field type") + origin = get_origin(annotation) + if origin is None: + return annotation + elif origin is Annotated: + return get_sa_type_from_type_annotation(get_args(annotation)[0]) + if _is_union_type(origin): + bases = get_args(annotation) + if len(bases) > 2: + raise ValueError("Cannot have a (non-optional) union as a SQLAlchemy field") + # Non optional unions are not allowed + if bases[0] is not NoneType and bases[1] is not NoneType: + raise ValueError("Cannot have a (non-optional) union as a SQLAlchemy field") + # Optional unions are allowed + use_type = bases[0] if bases[0] is not NoneType else bases[1] + return get_sa_type_from_type_annotation(use_type) + return origin + + +def get_sa_type_from_field(field: Any) -> Any: + type_: Any = field.annotation + return get_sa_type_from_type_annotation(type_) + + +def get_field_metadata(field: Any) -> Any: + for meta in field.metadata: + if isinstance(meta, (PydanticMetadata, MaxLen)): + return meta + return FakeMetadata() + + +def sqlmodel_table_construct( + *, + self_instance: _TSQLModel, + values: dict[str, Any], + _fields_set: Union[set[str], None] = None, +) -> _TSQLModel: + # Copy from Pydantic's BaseModel.construct() + # Ref: https://github.com/pydantic/pydantic/blob/v2.5.2/pydantic/main.py#L198 + # Modified to not include everything, only the model fields, and to + # set relationships + # SQLModel override to get class SQLAlchemy __dict__ attributes and + # set them back in after creating the object + # new_obj = cls.__new__(cls) + cls = type(self_instance) + old_dict = self_instance.__dict__.copy() + # End SQLModel override + + fields_values: dict[str, Any] = {} + defaults: dict[ + str, Any + ] = {} # keeping this separate from `fields_values` helps us compute `_fields_set` + for name, field in cls.model_fields.items(): + if field.alias and field.alias in values: + fields_values[name] = values.pop(field.alias) + elif name in values: + fields_values[name] = values.pop(name) + elif not field.is_required(): + defaults[name] = field.get_default(call_default_factory=True) + if _fields_set is None: + _fields_set = set(fields_values.keys()) + fields_values.update(defaults) + + _extra: Union[dict[str, Any], None] = None + if cls.model_config.get("extra") == "allow": + _extra = {} + for k, v in values.items(): + _extra[k] = v + # SQLModel override, do not include everything, only the model fields + # else: + # fields_values.update(values) + # End SQLModel override + # SQLModel override + # Do not set __dict__, instead use setattr to trigger SQLAlchemy + # object.__setattr__(new_obj, "__dict__", fields_values) + # instrumentation + for key, value in {**old_dict, **fields_values}.items(): + setattr(self_instance, key, value) + # End SQLModel override + object.__setattr__(self_instance, "__pydantic_fields_set__", _fields_set) + if not cls.__pydantic_root_model__: + object.__setattr__(self_instance, "__pydantic_extra__", _extra) + + if cls.__pydantic_post_init__: + self_instance.model_post_init(None) + elif not cls.__pydantic_root_model__: + # Note: if there are any private attributes, cls.__pydantic_post_init__ would exist + # Since it doesn't, that means that `__pydantic_private__` should be set to None + object.__setattr__(self_instance, "__pydantic_private__", None) + # SQLModel override, set relationships + # Get and set any relationship objects + for key in self_instance.__sqlmodel_relationships__: + value = values.get(key, Undefined) + if value is not Undefined: + setattr(self_instance, key, value) + # End SQLModel override + return self_instance + + +def sqlmodel_validate( + cls: type[_TSQLModel], + obj: Any, + *, + strict: Union[bool, None] = None, + from_attributes: Union[bool, None] = None, + context: Union[dict[str, Any], None] = None, + update: Union[dict[str, Any], None] = None, +) -> _TSQLModel: + if not is_table_model_class(cls): + new_obj: _TSQLModel = cls.__new__(cls) + else: + # If table, create the new instance normally to make SQLAlchemy create + # the _sa_instance_state attribute + # The wrapper of this function should use with _partial_init() + with partial_init(): + new_obj = cls() + # SQLModel Override to get class SQLAlchemy __dict__ attributes and + # set them back in after creating the object + old_dict = new_obj.__dict__.copy() + use_obj = obj + if isinstance(obj, dict) and update: + use_obj = {**obj, **update} + elif update: + use_obj = ObjectWithUpdateWrapper(obj=obj, update=update) + cls.__pydantic_validator__.validate_python( + use_obj, + strict=strict, + from_attributes=from_attributes, + context=context, + self_instance=new_obj, + ) + # Capture fields set to restore it later + fields_set = new_obj.__pydantic_fields_set__.copy() + if not is_table_model_class(cls): + # If not table, normal Pydantic code, set __dict__ + new_obj.__dict__ = {**old_dict, **new_obj.__dict__} + else: # Do not set __dict__, instead use setattr to trigger SQLAlchemy - # object.__setattr__(new_obj, "__dict__", fields_values) # instrumentation - for key, value in {**old_dict, **fields_values}.items(): - setattr(self_instance, key, value) - # End SQLModel override - object.__setattr__(self_instance, "__pydantic_fields_set__", _fields_set) - if not cls.__pydantic_root_model__: - object.__setattr__(self_instance, "__pydantic_extra__", _extra) - - if cls.__pydantic_post_init__: - self_instance.model_post_init(None) - elif not cls.__pydantic_root_model__: - # Note: if there are any private attributes, cls.__pydantic_post_init__ would exist - # Since it doesn't, that means that `__pydantic_private__` should be set to None - object.__setattr__(self_instance, "__pydantic_private__", None) - # SQLModel override, set relationships - # Get and set any relationship objects - for key in self_instance.__sqlmodel_relationships__: - value = values.get(key, Undefined) + for key, value in {**old_dict, **new_obj.__dict__}.items(): + setattr(new_obj, key, value) + # Restore fields set + object.__setattr__(new_obj, "__pydantic_fields_set__", fields_set) + # Get and set any relationship objects + if is_table_model_class(cls): + for key in new_obj.__sqlmodel_relationships__: + value = getattr(use_obj, key, Undefined) if value is not Undefined: - setattr(self_instance, key, value) - # End SQLModel override - return self_instance - - def sqlmodel_validate( - cls: type[_TSQLModel], - obj: Any, - *, - strict: Union[bool, None] = None, - from_attributes: Union[bool, None] = None, - context: Union[dict[str, Any], None] = None, - update: Union[dict[str, Any], None] = None, - ) -> _TSQLModel: - if not is_table_model_class(cls): - new_obj: _TSQLModel = cls.__new__(cls) - else: - # If table, create the new instance normally to make SQLAlchemy create - # the _sa_instance_state attribute - # The wrapper of this function should use with _partial_init() - with partial_init(): - new_obj = cls() - # SQLModel Override to get class SQLAlchemy __dict__ attributes and - # set them back in after creating the object - old_dict = new_obj.__dict__.copy() - use_obj = obj - if isinstance(obj, dict) and update: - use_obj = {**obj, **update} - elif update: - use_obj = ObjectWithUpdateWrapper(obj=obj, update=update) - cls.__pydantic_validator__.validate_python( - use_obj, - strict=strict, - from_attributes=from_attributes, - context=context, - self_instance=new_obj, - ) - # Capture fields set to restore it later - fields_set = new_obj.__pydantic_fields_set__.copy() - if not is_table_model_class(cls): - # If not table, normal Pydantic code, set __dict__ - new_obj.__dict__ = {**old_dict, **new_obj.__dict__} - else: - # Do not set __dict__, instead use setattr to trigger SQLAlchemy - # instrumentation - for key, value in {**old_dict, **new_obj.__dict__}.items(): setattr(new_obj, key, value) - # Restore fields set - object.__setattr__(new_obj, "__pydantic_fields_set__", fields_set) - # Get and set any relationship objects - if is_table_model_class(cls): - for key in new_obj.__sqlmodel_relationships__: - value = getattr(use_obj, key, Undefined) - if value is not Undefined: - setattr(new_obj, key, value) - return new_obj - - def sqlmodel_init(*, self: "SQLModel", data: dict[str, Any]) -> None: - old_dict = self.__dict__.copy() - if not is_table_model_class(self.__class__): - self.__pydantic_validator__.validate_python( - data, - self_instance=self, - ) - else: - sqlmodel_table_construct( - self_instance=self, - values=data, - ) - object.__setattr__( - self, - "__dict__", - {**old_dict, **self.__dict__}, - ) + return new_obj -else: - from pydantic import BaseConfig as BaseConfig # type: ignore[assignment] - from pydantic.errors import ConfigError - from pydantic.fields import ( # type: ignore[attr-defined, no-redef] - SHAPE_SINGLETON, - ModelField, - ) - from pydantic.fields import ( # type: ignore[attr-defined, no-redef] - Undefined as Undefined, # noqa - ) - from pydantic.fields import ( # type: ignore[attr-defined, no-redef] - UndefinedType as UndefinedType, - ) - from pydantic.main import ( # type: ignore[no-redef] - ModelMetaclass as ModelMetaclass, - ) - from pydantic.main import validate_model - from pydantic.typing import resolve_annotations - from pydantic.utils import ROOT_KEY, ValueItems - from pydantic.utils import ( # type: ignore[no-redef] - Representation as Representation, - ) - class SQLModelConfig(BaseConfig): # type: ignore[no-redef] - table: Optional[bool] = None # type: ignore[misc] - registry: Optional[Any] = None # type: ignore[misc] - - def get_config_value( - *, model: InstanceOrType["SQLModel"], parameter: str, default: Any = None - ) -> Any: - return getattr(model.__config__, parameter, default) # type: ignore[union-attr] - - def set_config_value( - *, - model: InstanceOrType["SQLModel"], - parameter: str, - value: Any, - ) -> None: - setattr(model.__config__, parameter, value) # type: ignore - - def get_model_fields(model: InstanceOrType[BaseModel]) -> dict[str, "FieldInfo"]: - return model.__fields__ # type: ignore - - def get_fields_set( - object: InstanceOrType["SQLModel"], - ) -> Union[set[str], Callable[[BaseModel], set[str]]]: - return object.__fields_set__ - - def init_pydantic_private_attrs(new_object: InstanceOrType["SQLModel"]) -> None: - object.__setattr__(new_object, "__fields_set__", set()) - - def get_annotations(class_dict: dict[str, Any]) -> dict[str, Any]: - return resolve_annotations( # type: ignore[no-any-return] - class_dict.get("__annotations__", {}), - class_dict.get("__module__", None), +def sqlmodel_init(*, self: "SQLModel", data: dict[str, Any]) -> None: + old_dict = self.__dict__.copy() + if not is_table_model_class(self.__class__): + self.__pydantic_validator__.validate_python( + data, + self_instance=self, ) - - def is_table_model_class(cls: type[Any]) -> bool: - config = getattr(cls, "__config__", None) - if config: - return getattr(config, "table", False) - return False - - def get_relationship_to( - name: str, - rel_info: "RelationshipInfo", - annotation: Any, - ) -> Any: - temp_field = ModelField.infer( # type: ignore[attr-defined] - name=name, - value=rel_info, - annotation=annotation, - class_validators=None, - config=SQLModelConfig, + else: + sqlmodel_table_construct( + self_instance=self, + values=data, ) - relationship_to = temp_field.type_ - if isinstance(temp_field.type_, ForwardRef): - relationship_to = temp_field.type_.__forward_arg__ - return relationship_to - - def is_field_noneable(field: "FieldInfo") -> bool: - if not field.required: # type: ignore[attr-defined] - # Taken from [Pydantic](https://github.com/samuelcolvin/pydantic/blob/v1.8.2/pydantic/fields.py#L946-L947) - return field.allow_none and ( # type: ignore[attr-defined] - field.shape != SHAPE_SINGLETON or not field.sub_fields # type: ignore[attr-defined] - ) - return field.allow_none # type: ignore[no-any-return, attr-defined] - - def get_sa_type_from_field(field: Any) -> Any: - if isinstance(field.type_, type) and field.shape == SHAPE_SINGLETON: - return field.type_ - raise ValueError(f"The field {field.name} has no matching SQLAlchemy type") - - def get_field_metadata(field: Any) -> Any: - metadata = FakeMetadata() - metadata.max_length = field.field_info.max_length - metadata.max_digits = getattr(field.type_, "max_digits", None) - metadata.decimal_places = getattr(field.type_, "decimal_places", None) - return metadata - - def post_init_field_info(field_info: FieldInfo) -> None: - field_info._validate() # type: ignore[attr-defined] - - def _calculate_keys( - self: "SQLModel", - include: Optional[Mapping[Union[int, str], Any]], - exclude: Optional[Mapping[Union[int, str], Any]], - exclude_unset: bool, - update: Optional[dict[str, Any]] = None, - ) -> Optional[Set[str]]: - if include is None and exclude is None and not exclude_unset: - # Original in Pydantic: - # return None - # Updated to not return SQLAlchemy attributes - # Do not include relationships as that would easily lead to infinite - # recursion, or traversing the whole database - return ( - self.__fields__.keys() # noqa - ) # | self.__sqlmodel_relationships__.keys() - - keys: Set[str] - if exclude_unset: - keys = self.__fields_set__.copy() # noqa - else: - # Original in Pydantic: - # keys = self.__dict__.keys() - # Updated to not return SQLAlchemy attributes - # Do not include relationships as that would easily lead to infinite - # recursion, or traversing the whole database - keys = ( - self.__fields__.keys() # noqa - ) # | self.__sqlmodel_relationships__.keys() - if include is not None: - keys &= include.keys() - - if update: - keys -= update.keys() - - if exclude: - keys -= {str(k) for k, v in exclude.items() if ValueItems.is_true(v)} - - return keys - - def sqlmodel_validate( - cls: type[_TSQLModel], - obj: Any, - *, - strict: Union[bool, None] = None, - from_attributes: Union[bool, None] = None, - context: Union[dict[str, Any], None] = None, - update: Union[dict[str, Any], None] = None, - ) -> _TSQLModel: - # This was SQLModel's original from_orm() for Pydantic v1 - # Duplicated from Pydantic - if not cls.__config__.orm_mode: # type: ignore[attr-defined] # noqa - raise ConfigError( - "You must have the config attribute orm_mode=True to use from_orm" - ) - if not isinstance(obj, Mapping): - obj = ( - {ROOT_KEY: obj} - if cls.__custom_root_type__ # type: ignore[attr-defined] # noqa - else cls._decompose_class(obj) # type: ignore[attr-defined] # noqa - ) - # SQLModel, support update dict - if update is not None: - obj = {**obj, **update} - # End SQLModel support dict - if not getattr(cls.__config__, "table", False): # noqa - # If not table, normal Pydantic code - m: _TSQLModel = cls.__new__(cls) - else: - # If table, create the new instance normally to make SQLAlchemy create - # the _sa_instance_state attribute - m = cls() - values, fields_set, validation_error = validate_model(cls, obj) - if validation_error: - raise validation_error - # Updated to trigger SQLAlchemy internal handling - if not getattr(cls.__config__, "table", False): # noqa - object.__setattr__(m, "__dict__", values) - else: - for key, value in values.items(): - setattr(m, key, value) - # Continue with standard Pydantic logic - object.__setattr__(m, "__fields_set__", fields_set) - m._init_private_attributes() # type: ignore[attr-defined] # noqa - return m - - def sqlmodel_init(*, self: "SQLModel", data: dict[str, Any]) -> None: - values, fields_set, validation_error = validate_model(self.__class__, data) - # Only raise errors if not a SQLModel model - if ( - not is_table_model_class(self.__class__) # noqa - and validation_error - ): - raise validation_error - if not is_table_model_class(self.__class__): - object.__setattr__(self, "__dict__", values) - else: - # Do not set values as in Pydantic, pass them through setattr, so - # SQLAlchemy can handle them - for key, value in values.items(): - setattr(self, key, value) - object.__setattr__(self, "__fields_set__", fields_set) - non_pydantic_keys = data.keys() - values.keys() - - if is_table_model_class(self.__class__): - for key in non_pydantic_keys: - if key in self.__sqlmodel_relationships__: - setattr(self, key, data[key]) + object.__setattr__( + self, + "__dict__", + {**old_dict, **self.__dict__}, + ) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 2e558647f1..fbc44de0e5 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -51,19 +51,15 @@ from typing_extensions import Literal, TypeAlias, deprecated, get_origin from ._compat import ( # type: ignore[attr-defined] - IS_PYDANTIC_V2, PYDANTIC_MINOR_VERSION, BaseConfig, - ModelField, ModelMetaclass, Representation, SQLModelConfig, Undefined, UndefinedType, - _calculate_keys, finish_init, get_annotations, - get_config_value, get_field_metadata, get_model_fields, get_relationship_to, @@ -71,8 +67,6 @@ init_pydantic_private_attrs, is_field_noneable, is_table_model_class, - post_init_field_info, - set_config_value, sqlmodel_init, sqlmodel_validate, ) @@ -414,26 +408,20 @@ def Field( "sa_column_kwargs": sa_column_kwargs, **current_schema_extra, } - if IS_PYDANTIC_V2: - # explicit params > schema_extra > alias propagation - field_info_kwargs["validation_alias"] = ( - validation_alias or schema_validation_alias or alias - ) - field_info_kwargs["serialization_alias"] = ( - serialization_alias or schema_serialization_alias or alias - ) - else: - if validation_alias or schema_validation_alias is not None: - raise RuntimeError("validation_alias is not supported in Pydantic v1") - if serialization_alias or schema_serialization_alias is not None: - raise RuntimeError("serialization_alias is not supported in Pydantic v1") + + # explicit params > schema_extra > alias propagation + field_info_kwargs["validation_alias"] = ( + validation_alias or schema_validation_alias or alias + ) + field_info_kwargs["serialization_alias"] = ( + serialization_alias or schema_serialization_alias or alias + ) + field_info = FieldInfo( default, default_factory=default_factory, **field_info_kwargs, ) - - post_init_field_info(field_info) return field_info @@ -487,8 +475,6 @@ class SQLModelMetaclass(ModelMetaclass, DeclarativeMeta): __sqlmodel_relationships__: dict[str, RelationshipInfo] model_config: SQLModelConfig model_fields: ClassVar[dict[str, FieldInfo]] - __config__: type[SQLModelConfig] - __fields__: dict[str, ModelField] # type: ignore[assignment] # Replicate SQLAlchemy def __setattr__(cls, name: str, value: Any) -> None: @@ -553,9 +539,7 @@ def __new__( } def get_config(name: str) -> Any: - config_class_value = get_config_value( - model=new_cls, parameter=name, default=Undefined - ) + config_class_value = new_cls.model_config.get(name, Undefined) if config_class_value is not Undefined: return config_class_value kwarg_value = kwargs.get(name, Undefined) @@ -566,7 +550,7 @@ def get_config(name: str) -> Any: config_table = get_config("table") if config_table is True: # If it was passed by kwargs, ensure it's also set in config - set_config_value(model=new_cls, parameter="table", value=config_table) + new_cls.model_config["table"] = config_table for k, v in get_model_fields(new_cls).items(): col = get_column_from_field(v) setattr(new_cls, k, col) @@ -575,18 +559,16 @@ def get_config(name: str) -> Any: # This could be done by reading new_cls.model_config['table'] in FastAPI, but # that's very specific about SQLModel, so let's have another config that # other future tools based on Pydantic can use. - set_config_value( - model=new_cls, parameter="read_from_attributes", value=True - ) + new_cls.model_config["read_from_attributes"] = True # For compatibility with older versions # TODO: remove this in the future - set_config_value(model=new_cls, parameter="read_with_orm_mode", value=True) + new_cls.model_config["read_with_orm_mode"] = True config_registry = get_config("registry") if config_registry is not Undefined: config_registry = cast(registry, config_registry) # If it was passed by kwargs, ensure it's also set in config - set_config_value(model=new_cls, parameter="registry", value=config_table) + new_cls.model_config["registry"] = config_table setattr(new_cls, "_sa_registry", config_registry) # noqa: B010 setattr(new_cls, "metadata", config_registry.metadata) # noqa: B010 setattr(new_cls, "__abstract__", True) # noqa: B010 @@ -653,10 +635,7 @@ def __init__( def get_sqlalchemy_type(field: Any) -> Any: - if IS_PYDANTIC_V2: - field_info = field - else: - field_info = field.field_info + field_info = field sa_type = getattr(field_info, "sa_type", Undefined) # noqa: B009 if sa_type is not Undefined: return sa_type @@ -710,10 +689,7 @@ def get_sqlalchemy_type(field: Any) -> Any: def get_column_from_field(field: Any) -> Column: # type: ignore - if IS_PYDANTIC_V2: - field_info = field - else: - field_info = field.field_info + field_info = field sa_column = getattr(field_info, "sa_column", Undefined) if isinstance(sa_column, Column): return sa_column @@ -784,13 +760,7 @@ class SQLModel(BaseModel, metaclass=SQLModelMetaclass, registry=default_registry __name__: ClassVar[str] metadata: ClassVar[MetaData] __allow_unmapped__ = True # https://docs.sqlalchemy.org/en/20/changelog/migration_20.html#migration-20-step-six - - if IS_PYDANTIC_V2: - model_config = SQLModelConfig(from_attributes=True) - else: - - class Config: - orm_mode = True + model_config = SQLModelConfig(from_attributes=True) def __new__(cls, *args: Any, **kwargs: Any) -> Any: new_object = super().__new__(cls) @@ -886,35 +856,24 @@ def model_dump( if PYDANTIC_MINOR_VERSION < (2, 11): by_alias = by_alias or False extra_kwargs: dict[str, Any] = {} - if PYDANTIC_MINOR_VERSION >= (2, 7): - extra_kwargs["context"] = context - extra_kwargs["serialize_as_any"] = serialize_as_any + extra_kwargs["context"] = context + extra_kwargs["serialize_as_any"] = serialize_as_any if PYDANTIC_MINOR_VERSION >= (2, 11): extra_kwargs["fallback"] = fallback if PYDANTIC_MINOR_VERSION >= (2, 12): extra_kwargs["exclude_computed_fields"] = exclude_computed_fields - if IS_PYDANTIC_V2: - return super().model_dump( - mode=mode, - include=include, - exclude=exclude, - by_alias=by_alias, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - round_trip=round_trip, - warnings=warnings, - **extra_kwargs, - ) - else: - return super().dict( - include=include, - exclude=exclude, - by_alias=by_alias or False, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) + return super().model_dump( + mode=mode, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + round_trip=round_trip, + warnings=warnings, + **extra_kwargs, + ) @deprecated( """ @@ -967,35 +926,8 @@ def parse_obj( obj: Any, update: Optional[builtins.dict[str, Any]] = None, ) -> _TSQLModel: - if not IS_PYDANTIC_V2: - obj = cls._enforce_dict_if_root(obj) # type: ignore[attr-defined] # noqa return cls.model_validate(obj, update=update) - # From Pydantic, override to only show keys from fields, omit SQLAlchemy attributes - @deprecated( - """ - 🚨 You should not access `obj._calculate_keys()` directly. - - It is only useful for Pydantic v1.X, you should probably upgrade to - Pydantic v2.X. - """, - category=None, - ) - def _calculate_keys( - self, - include: Optional[Mapping[Union[int, str], Any]], - exclude: Optional[Mapping[Union[int, str], Any]], - exclude_unset: bool, - update: Optional[builtins.dict[str, Any]] = None, - ) -> Optional[Set[str]]: - return _calculate_keys( - self, - include=include, - exclude=exclude, - exclude_unset=exclude_unset, - update=update, - ) - def sqlmodel_update( self: _TSQLModel, obj: Union[builtins.dict[str, Any], BaseModel], diff --git a/tests/conftest.py b/tests/conftest.py index 39597095b0..9f5f8cf36a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,6 @@ import pytest from pydantic import BaseModel from sqlmodel import SQLModel -from sqlmodel._compat import IS_PYDANTIC_V2 from sqlmodel.main import default_registry top_level_path = Path(__file__).resolve().parent.parent @@ -87,9 +86,6 @@ def print_mock_fixture() -> Generator[PrintMock, None, None]: yield print_mock -needs_pydanticv2 = pytest.mark.skipif(not IS_PYDANTIC_V2, reason="requires Pydantic v2") -needs_pydanticv1 = pytest.mark.skipif(IS_PYDANTIC_V2, reason="requires Pydantic v1") - needs_py310 = pytest.mark.skipif( sys.version_info < (3, 10), reason="requires python3.10+" ) diff --git a/tests/test_aliases.py b/tests/test_aliases.py index cbd5030c73..f123251e09 100644 --- a/tests/test_aliases.py +++ b/tests/test_aliases.py @@ -4,9 +4,6 @@ from pydantic import BaseModel, ValidationError from pydantic import Field as PField from sqlmodel import Field, SQLModel -from sqlmodel._compat import IS_PYDANTIC_V2 - -from tests.conftest import needs_pydanticv1, needs_pydanticv2 """ Alias tests for SQLModel and Pydantic compatibility @@ -22,23 +19,12 @@ class SQLModelUser(SQLModel): # Models with config (validate_by_name=True) -if IS_PYDANTIC_V2: - - class PydanticUserWithConfig(PydanticUser): - model_config = {"validate_by_name": True} - - class SQLModelUserWithConfig(SQLModelUser): - model_config = {"validate_by_name": True} - -else: +class PydanticUserWithConfig(PydanticUser): + model_config = {"validate_by_name": True} - class PydanticUserWithConfig(PydanticUser): - class Config: - allow_population_by_field_name = True - class SQLModelUserWithConfig(SQLModelUser): - class Config: - allow_population_by_field_name = True +class SQLModelUserWithConfig(SQLModelUser): + model_config = {"validate_by_name": True} @pytest.mark.parametrize("model", [PydanticUser, SQLModelUser]) @@ -84,10 +70,7 @@ def test_dict_default_uses_field_names( model: Union[type[PydanticUser], type[SQLModelUser]], ): user = model(fullName="Dana") - if IS_PYDANTIC_V2 or isinstance(user, SQLModel): - data = user.model_dump() - else: - data = user.dict() + data = user.model_dump() assert "full_name" in data assert "fullName" not in data assert data["full_name"] == "Dana" @@ -98,10 +81,7 @@ def test_dict_by_alias_uses_aliases( model: Union[type[PydanticUser], type[SQLModelUser]], ): user = model(fullName="Dana") - if IS_PYDANTIC_V2 or isinstance(user, SQLModel): - data = user.model_dump(by_alias=True) - else: - data = user.dict(by_alias=True) + data = user.model_dump(by_alias=True) assert "fullName" in data assert "full_name" not in data assert data["fullName"] == "Dana" @@ -112,48 +92,19 @@ def test_json_by_alias( model: Union[type[PydanticUser], type[SQLModelUser]], ): user = model(fullName="Frank") - if IS_PYDANTIC_V2: - json_data = user.model_dump_json(by_alias=True) - else: - json_data = user.json(by_alias=True) + json_data = user.model_dump_json(by_alias=True) assert ('"fullName":"Frank"' in json_data) or ('"fullName": "Frank"' in json_data) assert "full_name" not in json_data -if IS_PYDANTIC_V2: - - class PydanticUserV2(BaseModel): - first_name: str = PField( - validation_alias="firstName", serialization_alias="f_name" - ) - - class SQLModelUserV2(SQLModel): - first_name: str = Field( - validation_alias="firstName", serialization_alias="f_name" - ) -else: - # Dummy classes for Pydantic v1 to prevent import errors - PydanticUserV2 = None - SQLModelUserV2 = None +class PydanticUserV2(BaseModel): + first_name: str = PField(validation_alias="firstName", serialization_alias="f_name") -@needs_pydanticv1 -def test_validation_alias_runtimeerror_pydantic_v1(): - with pytest.raises( - RuntimeError, match="validation_alias is not supported in Pydantic v1" - ): - Field(validation_alias="foo") +class SQLModelUserV2(SQLModel): + first_name: str = Field(validation_alias="firstName", serialization_alias="f_name") -@needs_pydanticv1 -def test_serialization_alias_runtimeerror_pydantic_v1(): - with pytest.raises( - RuntimeError, match="serialization_alias is not supported in Pydantic v1" - ): - Field(serialization_alias="bar") - - -@needs_pydanticv2 @pytest.mark.parametrize("model", [PydanticUserV2, SQLModelUserV2]) def test_create_with_validation_alias( model: Union[type[PydanticUserV2], type[SQLModelUserV2]], @@ -162,7 +113,6 @@ def test_create_with_validation_alias( assert user.first_name == "John" -@needs_pydanticv2 @pytest.mark.parametrize("model", [PydanticUserV2, SQLModelUserV2]) def test_serialize_with_serialization_alias( model: Union[type[PydanticUserV2], type[SQLModelUserV2]], @@ -175,7 +125,6 @@ def test_serialize_with_serialization_alias( assert data["f_name"] == "Jane" -@needs_pydanticv2 def test_schema_extra_validation_alias_sqlmodel_v2(): class M(SQLModel): f: str = Field(schema_extra={"validation_alias": "f_alias"}) @@ -184,7 +133,6 @@ class M(SQLModel): assert m.f == "asd" -@needs_pydanticv2 def test_schema_extra_serialization_alias_sqlmodel_v2(): class M(SQLModel): f: str = Field(schema_extra={"serialization_alias": "f_out"}) @@ -196,23 +144,6 @@ class M(SQLModel): assert data["f_out"] == "x" -@needs_pydanticv1 -def test_schema_extra_validation_alias_runtimeerror_pydantic_v1(): - with pytest.raises( - RuntimeError, match="validation_alias is not supported in Pydantic v1" - ): - Field(schema_extra={"validation_alias": "x"}) - - -@needs_pydanticv1 -def test_schema_extra_serialization_alias_runtimeerror_pydantic_v1(): - with pytest.raises( - RuntimeError, match="serialization_alias is not supported in Pydantic v1" - ): - Field(schema_extra={"serialization_alias": "y"}) - - -@needs_pydanticv2 def test_alias_plus_validation_alias_prefers_validation_alias_sqlmodel_v2(): class M(SQLModel): first_name: str = Field(alias="fullName", validation_alias="v_name") @@ -221,7 +152,6 @@ class M(SQLModel): assert m.first_name == "B" -@needs_pydanticv2 def test_alias_plus_serialization_alias_prefers_serialization_alias_sqlmodel_v2(): class M(SQLModel): first_name: str = Field(alias="fullName", serialization_alias="f_name") @@ -233,7 +163,6 @@ class M(SQLModel): assert data["f_name"] == "Z" -@needs_pydanticv2 def test_alias_generator_works_sqlmodel_v2(): class M(SQLModel): model_config = {"alias_generator": lambda s: "gen_" + s} @@ -245,7 +174,6 @@ class M(SQLModel): assert "gen_f" in data and data["gen_f"] == "ok" -@needs_pydanticv2 def test_alias_generator_with_explicit_alias_prefers_field_alias_sqlmodel_v2(): class M(SQLModel): model_config = {"alias_generator": lambda s: "gen_" + s} diff --git a/tests/test_annotated_uuid.py b/tests/test_annotated_uuid.py index b0e25ab099..6e22d60698 100644 --- a/tests/test_annotated_uuid.py +++ b/tests/test_annotated_uuid.py @@ -3,10 +3,7 @@ from sqlmodel import Field, Session, SQLModel, create_engine, select -from tests.conftest import needs_pydanticv2 - -@needs_pydanticv2 def test_annotated_optional_types(clear_sqlmodel) -> None: from pydantic import UUID4 diff --git a/tests/test_enums.py b/tests/test_enums.py index 2808f3f9a9..933a24e99e 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -6,7 +6,6 @@ from sqlmodel import SQLModel from . import test_enums_models -from .conftest import needs_pydanticv1, needs_pydanticv2 """ Tests related to Enums @@ -55,49 +54,6 @@ def test_sqlite_ddl_sql(clear_sqlmodel, capsys: pytest.CaptureFixture[str]): assert "CREATE TYPE" not in captured.out -@needs_pydanticv1 -def test_json_schema_flat_model_pydantic_v1(): - assert test_enums_models.FlatModel.schema() == { - "title": "FlatModel", - "type": "object", - "properties": { - "id": {"title": "Id", "type": "string", "format": "uuid"}, - "enum_field": {"$ref": "#/definitions/MyEnum1"}, - }, - "required": ["id", "enum_field"], - "definitions": { - "MyEnum1": { - "title": "MyEnum1", - "description": "An enumeration.", - "enum": ["A", "B"], - "type": "string", - } - }, - } - - -@needs_pydanticv1 -def test_json_schema_inherit_model_pydantic_v1(): - assert test_enums_models.InheritModel.schema() == { - "title": "InheritModel", - "type": "object", - "properties": { - "id": {"title": "Id", "type": "string", "format": "uuid"}, - "enum_field": {"$ref": "#/definitions/MyEnum2"}, - }, - "required": ["id", "enum_field"], - "definitions": { - "MyEnum2": { - "title": "MyEnum2", - "description": "An enumeration.", - "enum": ["C", "D"], - "type": "string", - } - }, - } - - -@needs_pydanticv2 def test_json_schema_flat_model_pydantic_v2(): assert test_enums_models.FlatModel.model_json_schema() == { "title": "FlatModel", @@ -113,7 +69,6 @@ def test_json_schema_flat_model_pydantic_v2(): } -@needs_pydanticv2 def test_json_schema_inherit_model_pydantic_v2(): assert test_enums_models.InheritModel.model_json_schema() == { "title": "InheritModel", diff --git a/tests/test_fields_set.py b/tests/test_fields_set.py index e0bd8cba76..0574e5a200 100644 --- a/tests/test_fields_set.py +++ b/tests/test_fields_set.py @@ -1,7 +1,6 @@ from datetime import datetime, timedelta from sqlmodel import Field, SQLModel -from sqlmodel._compat import get_fields_set def test_fields_set(): @@ -11,12 +10,12 @@ class User(SQLModel): last_updated: datetime = Field(default_factory=datetime.now) user = User(username="bob") - assert get_fields_set(user) == {"username"} + assert user.model_fields_set == {"username"} user = User(username="bob", email="bob@test.com") - assert get_fields_set(user) == {"username", "email"} + assert user.model_fields_set == {"username", "email"} user = User( username="bob", email="bob@test.com", last_updated=datetime.now() - timedelta(days=1), ) - assert get_fields_set(user) == {"username", "email", "last_updated"} + assert user.model_fields_set == {"username", "email", "last_updated"} diff --git a/tests/test_validation.py b/tests/test_validation.py index 3265922070..fbea2f7b97 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -4,39 +4,7 @@ from pydantic.error_wrappers import ValidationError from sqlmodel import SQLModel -from .conftest import needs_pydanticv1, needs_pydanticv2 - -@needs_pydanticv1 -def test_validation_pydantic_v1(clear_sqlmodel): - """Test validation of implicit and explicit None values. - - # For consistency with pydantic, validators are not to be called on - # arguments that are not explicitly provided. - - https://github.com/tiangolo/sqlmodel/issues/230 - https://github.com/samuelcolvin/pydantic/issues/1223 - - """ - from pydantic import validator - - class Hero(SQLModel): - name: Optional[str] = None - secret_name: Optional[str] = None - age: Optional[int] = None - - @validator("name", "secret_name", "age") - def reject_none(cls, v): - assert v is not None - return v - - Hero.validate({"age": 25}) - - with pytest.raises(ValidationError): - Hero.validate({"name": None, "age": 25}) - - -@needs_pydanticv2 def test_validation_pydantic_v2(clear_sqlmodel): """Test validation of implicit and explicit None values. diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000000..b52e0f35a5 --- /dev/null +++ b/uv.lock @@ -0,0 +1,388 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version < '3.10'", +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "greenlet" +version = "3.2.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/ed/6bfa4109fcb23a58819600392564fea69cdc6551ffd5e69ccf1d52a40cbc/greenlet-3.2.4-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c", size = 271061, upload-time = "2025-08-07T13:17:15.373Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fc/102ec1a2fc015b3a7652abab7acf3541d58c04d3d17a8d3d6a44adae1eb1/greenlet-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590", size = 629475, upload-time = "2025-08-07T13:42:54.009Z" }, + { url = "https://files.pythonhosted.org/packages/c5/26/80383131d55a4ac0fb08d71660fd77e7660b9db6bdb4e8884f46d9f2cc04/greenlet-3.2.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f10fd42b5ee276335863712fa3da6608e93f70629c631bf77145021600abc23c", size = 640802, upload-time = "2025-08-07T13:45:25.52Z" }, + { url = "https://files.pythonhosted.org/packages/9f/7c/e7833dbcd8f376f3326bd728c845d31dcde4c84268d3921afcae77d90d08/greenlet-3.2.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c8c9e331e58180d0d83c5b7999255721b725913ff6bc6cf39fa2a45841a4fd4b", size = 636703, upload-time = "2025-08-07T13:53:12.622Z" }, + { url = "https://files.pythonhosted.org/packages/e9/49/547b93b7c0428ede7b3f309bc965986874759f7d89e4e04aeddbc9699acb/greenlet-3.2.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58b97143c9cc7b86fc458f215bd0932f1757ce649e05b640fea2e79b54cedb31", size = 635417, upload-time = "2025-08-07T13:18:25.189Z" }, + { url = "https://files.pythonhosted.org/packages/7f/91/ae2eb6b7979e2f9b035a9f612cf70f1bf54aad4e1d125129bef1eae96f19/greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d", size = 584358, upload-time = "2025-08-07T13:18:23.708Z" }, + { url = "https://files.pythonhosted.org/packages/f7/85/433de0c9c0252b22b16d413c9407e6cb3b41df7389afc366ca204dbc1393/greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5", size = 1113550, upload-time = "2025-08-07T13:42:37.467Z" }, + { url = "https://files.pythonhosted.org/packages/a1/8d/88f3ebd2bc96bf7747093696f4335a0a8a4c5acfcf1b757717c0d2474ba3/greenlet-3.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f", size = 1137126, upload-time = "2025-08-07T13:18:20.239Z" }, + { url = "https://files.pythonhosted.org/packages/f1/29/74242b7d72385e29bcc5563fba67dad94943d7cd03552bac320d597f29b2/greenlet-3.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f47617f698838ba98f4ff4189aef02e7343952df3a615f847bb575c3feb177a7", size = 1544904, upload-time = "2025-11-04T12:42:04.763Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e2/1572b8eeab0f77df5f6729d6ab6b141e4a84ee8eb9bc8c1e7918f94eda6d/greenlet-3.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af41be48a4f60429d5cad9d22175217805098a9ef7c40bfef44f7669fb9d74d8", size = 1611228, upload-time = "2025-11-04T12:42:08.423Z" }, + { url = "https://files.pythonhosted.org/packages/d6/6f/b60b0291d9623c496638c582297ead61f43c4b72eef5e9c926ef4565ec13/greenlet-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c", size = 298654, upload-time = "2025-08-07T13:50:00.469Z" }, + { url = "https://files.pythonhosted.org/packages/a4/de/f28ced0a67749cac23fecb02b694f6473f47686dff6afaa211d186e2ef9c/greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", size = 272305, upload-time = "2025-08-07T13:15:41.288Z" }, + { url = "https://files.pythonhosted.org/packages/09/16/2c3792cba130000bf2a31c5272999113f4764fd9d874fb257ff588ac779a/greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", size = 632472, upload-time = "2025-08-07T13:42:55.044Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/95d48d7e3d433e6dae5b1682e4292242a53f22df82e6d3dda81b1701a960/greenlet-3.2.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3", size = 644646, upload-time = "2025-08-07T13:45:26.523Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5e/405965351aef8c76b8ef7ad370e5da58d57ef6068df197548b015464001a/greenlet-3.2.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633", size = 640519, upload-time = "2025-08-07T13:53:13.928Z" }, + { url = "https://files.pythonhosted.org/packages/25/5d/382753b52006ce0218297ec1b628e048c4e64b155379331f25a7316eb749/greenlet-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079", size = 639707, upload-time = "2025-08-07T13:18:27.146Z" }, + { url = "https://files.pythonhosted.org/packages/1f/8e/abdd3f14d735b2929290a018ecf133c901be4874b858dd1c604b9319f064/greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", size = 587684, upload-time = "2025-08-07T13:18:25.164Z" }, + { url = "https://files.pythonhosted.org/packages/5d/65/deb2a69c3e5996439b0176f6651e0052542bb6c8f8ec2e3fba97c9768805/greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", size = 1116647, upload-time = "2025-08-07T13:42:38.655Z" }, + { url = "https://files.pythonhosted.org/packages/3f/cc/b07000438a29ac5cfb2194bfc128151d52f333cee74dd7dfe3fb733fc16c/greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa", size = 1142073, upload-time = "2025-08-07T13:18:21.737Z" }, + { url = "https://files.pythonhosted.org/packages/67/24/28a5b2fa42d12b3d7e5614145f0bd89714c34c08be6aabe39c14dd52db34/greenlet-3.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9c6de1940a7d828635fbd254d69db79e54619f165ee7ce32fda763a9cb6a58c", size = 1548385, upload-time = "2025-11-04T12:42:11.067Z" }, + { url = "https://files.pythonhosted.org/packages/6a/05/03f2f0bdd0b0ff9a4f7b99333d57b53a7709c27723ec8123056b084e69cd/greenlet-3.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03c5136e7be905045160b1b9fdca93dd6727b180feeafda6818e6496434ed8c5", size = 1613329, upload-time = "2025-11-04T12:42:12.928Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0f/30aef242fcab550b0b3520b8e3561156857c94288f0332a79928c31a52cf/greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9", size = 299100, upload-time = "2025-08-07T13:44:12.287Z" }, + { url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" }, + { url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" }, + { url = "https://files.pythonhosted.org/packages/3b/16/035dcfcc48715ccd345f3a93183267167cdd162ad123cd93067d86f27ce4/greenlet-3.2.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968", size = 655185, upload-time = "2025-08-07T13:45:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/31/da/0386695eef69ffae1ad726881571dfe28b41970173947e7c558d9998de0f/greenlet-3.2.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9", size = 649926, upload-time = "2025-08-07T13:53:15.251Z" }, + { url = "https://files.pythonhosted.org/packages/68/88/69bf19fd4dc19981928ceacbc5fd4bb6bc2215d53199e367832e98d1d8fe/greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6", size = 651839, upload-time = "2025-08-07T13:18:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c7/12381b18e21aef2c6bd3a636da1088b888b97b7a0362fac2e4de92405f97/greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", size = 1151142, upload-time = "2025-08-07T13:18:22.981Z" }, + { url = "https://files.pythonhosted.org/packages/27/45/80935968b53cfd3f33cf99ea5f08227f2646e044568c9b1555b58ffd61c2/greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0", size = 1564846, upload-time = "2025-11-04T12:42:15.191Z" }, + { url = "https://files.pythonhosted.org/packages/69/02/b7c30e5e04752cb4db6202a3858b149c0710e5453b71a3b2aec5d78a1aab/greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d", size = 1633814, upload-time = "2025-11-04T12:42:17.175Z" }, + { url = "https://files.pythonhosted.org/packages/e9/08/b0814846b79399e585f974bbeebf5580fbe59e258ea7be64d9dfb253c84f/greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", size = 299899, upload-time = "2025-08-07T13:38:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, + { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, + { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, + { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, + { url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" }, + { url = "https://files.pythonhosted.org/packages/1c/53/f9c440463b3057485b8594d7a638bed53ba531165ef0ca0e6c364b5cc807/greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b", size = 1564759, upload-time = "2025-11-04T12:42:19.395Z" }, + { url = "https://files.pythonhosted.org/packages/47/e4/3bb4240abdd0a8d23f4f88adec746a3099f0d86bfedb623f063b2e3b4df0/greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929", size = 1634288, upload-time = "2025-11-04T12:42:21.174Z" }, + { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" }, + { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, + { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, + { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" }, + { url = "https://files.pythonhosted.org/packages/0d/da/343cd760ab2f92bac1845ca07ee3faea9fe52bee65f7bcb19f16ad7de08b/greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681", size = 1680760, upload-time = "2025-11-04T12:42:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c0/93885c4106d2626bf51fdec377d6aef740dfa5c4877461889a7cf8e565cc/greenlet-3.2.4-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:b6a7c19cf0d2742d0809a4c05975db036fdff50cd294a93632d6a310bf9ac02c", size = 269859, upload-time = "2025-08-07T13:16:16.003Z" }, + { url = "https://files.pythonhosted.org/packages/4d/f5/33f05dc3ba10a02dedb1485870cf81c109227d3d3aa280f0e48486cac248/greenlet-3.2.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:27890167f55d2387576d1f41d9487ef171849ea0359ce1510ca6e06c8bece11d", size = 627610, upload-time = "2025-08-07T13:43:01.345Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a7/9476decef51a0844195f99ed5dc611d212e9b3515512ecdf7321543a7225/greenlet-3.2.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:18d9260df2b5fbf41ae5139e1be4e796d99655f023a636cd0e11e6406cca7d58", size = 639417, upload-time = "2025-08-07T13:45:32.094Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e0/849b9159cbb176f8c0af5caaff1faffdece7a8417fcc6fe1869770e33e21/greenlet-3.2.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:671df96c1f23c4a0d4077a325483c1503c96a1b7d9db26592ae770daa41233d4", size = 634751, upload-time = "2025-08-07T13:53:18.848Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d3/844e714a9bbd39034144dca8b658dcd01839b72bb0ec7d8014e33e3705f0/greenlet-3.2.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:16458c245a38991aa19676900d48bd1a6f2ce3e16595051a4db9d012154e8433", size = 634020, upload-time = "2025-08-07T13:18:36.841Z" }, + { url = "https://files.pythonhosted.org/packages/6b/4c/f3de2a8de0e840ecb0253ad0dc7e2bb3747348e798ec7e397d783a3cb380/greenlet-3.2.4-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9913f1a30e4526f432991f89ae263459b1c64d1608c0d22a5c79c287b3c70df", size = 582817, upload-time = "2025-08-07T13:18:35.48Z" }, + { url = "https://files.pythonhosted.org/packages/89/80/7332915adc766035c8980b161c2e5d50b2f941f453af232c164cff5e0aeb/greenlet-3.2.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b90654e092f928f110e0007f572007c9727b5265f7632c2fa7415b4689351594", size = 1111985, upload-time = "2025-08-07T13:42:42.425Z" }, + { url = "https://files.pythonhosted.org/packages/66/71/1928e2c80197353bcb9b50aa19c4d8e26ee6d7a900c564907665cf4b9a41/greenlet-3.2.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81701fd84f26330f0d5f4944d4e92e61afe6319dcd9775e39396e39d7c3e5f98", size = 1136137, upload-time = "2025-08-07T13:18:26.168Z" }, + { url = "https://files.pythonhosted.org/packages/4b/bf/7bd33643e48ed45dcc0e22572f650767832bd4e1287f97434943cc402148/greenlet-3.2.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:28a3c6b7cd72a96f61b0e4b2a36f681025b60ae4779cc73c1535eb5f29560b10", size = 1542941, upload-time = "2025-11-04T12:42:27.427Z" }, + { url = "https://files.pythonhosted.org/packages/9b/74/4bc433f91d0d09a1c22954a371f9df928cb85e72640870158853a83415e5/greenlet-3.2.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:52206cd642670b0b320a1fd1cbfd95bca0e043179c1d8a045f2c6109dfe973be", size = 1609685, upload-time = "2025-11-04T12:42:29.242Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/a5dc74dde38aeb2b15d418cec76ed50e1dd3d620ccda84d8199703248968/greenlet-3.2.4-cp39-cp39-win32.whl", hash = "sha256:65458b409c1ed459ea899e939f0e1cdb14f58dbc803f2f93c5eab5694d32671b", size = 281400, upload-time = "2025-08-07T14:02:20.263Z" }, + { url = "https://files.pythonhosted.org/packages/e5/44/342c4591db50db1076b8bda86ed0ad59240e3e1da17806a4cf10a6d0e447/greenlet-3.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb", size = 298533, upload-time = "2025-08-07T13:56:34.168Z" }, +] + +[[package]] +name = "greenlet" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651, upload-time = "2025-12-04T14:49:44.05Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/6a/33d1702184d94106d3cdd7bfb788e19723206fce152e303473ca3b946c7b/greenlet-3.3.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:6f8496d434d5cb2dce025773ba5597f71f5410ae499d5dd9533e0653258cdb3d", size = 273658, upload-time = "2025-12-04T14:23:37.494Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b7/2b5805bbf1907c26e434f4e448cd8b696a0b71725204fa21a211ff0c04a7/greenlet-3.3.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b96dc7eef78fd404e022e165ec55327f935b9b52ff355b067eb4a0267fc1cffb", size = 574810, upload-time = "2025-12-04T14:50:04.154Z" }, + { url = "https://files.pythonhosted.org/packages/94/38/343242ec12eddf3d8458c73f555c084359883d4ddc674240d9e61ec51fd6/greenlet-3.3.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73631cd5cccbcfe63e3f9492aaa664d278fda0ce5c3d43aeda8e77317e38efbd", size = 586248, upload-time = "2025-12-04T14:57:39.35Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/0ae86792fb212e4384041e0ef8e7bc66f59a54912ce407d26a966ed2914d/greenlet-3.3.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b299a0cb979f5d7197442dccc3aee67fce53500cd88951b7e6c35575701c980b", size = 597403, upload-time = "2025-12-04T15:07:10.831Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a8/15d0aa26c0036a15d2659175af00954aaaa5d0d66ba538345bd88013b4d7/greenlet-3.3.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dee147740789a4632cace364816046e43310b59ff8fb79833ab043aefa72fd5", size = 586910, upload-time = "2025-12-04T14:25:59.705Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9b/68d5e3b7ccaba3907e5532cf8b9bf16f9ef5056a008f195a367db0ff32db/greenlet-3.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:39b28e339fc3c348427560494e28d8a6f3561c8d2bcf7d706e1c624ed8d822b9", size = 1547206, upload-time = "2025-12-04T15:04:21.027Z" }, + { url = "https://files.pythonhosted.org/packages/66/bd/e3086ccedc61e49f91e2cfb5ffad9d8d62e5dc85e512a6200f096875b60c/greenlet-3.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b3c374782c2935cc63b2a27ba8708471de4ad1abaa862ffdb1ef45a643ddbb7d", size = 1613359, upload-time = "2025-12-04T14:27:26.548Z" }, + { url = "https://files.pythonhosted.org/packages/f4/6b/d4e73f5dfa888364bbf02efa85616c6714ae7c631c201349782e5b428925/greenlet-3.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:b49e7ed51876b459bd645d83db257f0180e345d3f768a35a85437a24d5a49082", size = 300740, upload-time = "2025-12-04T14:47:52.773Z" }, + { url = "https://files.pythonhosted.org/packages/1f/cb/48e964c452ca2b92175a9b2dca037a553036cb053ba69e284650ce755f13/greenlet-3.3.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e29f3018580e8412d6aaf5641bb7745d38c85228dacf51a73bd4e26ddf2a6a8e", size = 274908, upload-time = "2025-12-04T14:23:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/28/da/38d7bff4d0277b594ec557f479d65272a893f1f2a716cad91efeb8680953/greenlet-3.3.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a687205fb22794e838f947e2194c0566d3812966b41c78709554aa883183fb62", size = 577113, upload-time = "2025-12-04T14:50:05.493Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f2/89c5eb0faddc3ff014f1c04467d67dee0d1d334ab81fadbf3744847f8a8a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4243050a88ba61842186cb9e63c7dfa677ec146160b0efd73b855a3d9c7fcf32", size = 590338, upload-time = "2025-12-04T14:57:41.136Z" }, + { url = "https://files.pythonhosted.org/packages/80/d7/db0a5085035d05134f8c089643da2b44cc9b80647c39e93129c5ef170d8f/greenlet-3.3.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:670d0f94cd302d81796e37299bcd04b95d62403883b24225c6b5271466612f45", size = 601098, upload-time = "2025-12-04T15:07:11.898Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a6/e959a127b630a58e23529972dbc868c107f9d583b5a9f878fb858c46bc1a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb3a8ec3db4a3b0eb8a3c25436c2d49e3505821802074969db017b87bc6a948", size = 590206, upload-time = "2025-12-04T14:26:01.254Z" }, + { url = "https://files.pythonhosted.org/packages/48/60/29035719feb91798693023608447283b266b12efc576ed013dd9442364bb/greenlet-3.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2de5a0b09eab81fc6a382791b995b1ccf2b172a9fec934747a7a23d2ff291794", size = 1550668, upload-time = "2025-12-04T15:04:22.439Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5f/783a23754b691bfa86bd72c3033aa107490deac9b2ef190837b860996c9f/greenlet-3.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4449a736606bd30f27f8e1ff4678ee193bc47f6ca810d705981cfffd6ce0d8c5", size = 1615483, upload-time = "2025-12-04T14:27:28.083Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d5/c339b3b4bc8198b7caa4f2bd9fd685ac9f29795816d8db112da3d04175bb/greenlet-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:7652ee180d16d447a683c04e4c5f6441bae7ba7b17ffd9f6b3aff4605e9e6f71", size = 301164, upload-time = "2025-12-04T14:42:51.577Z" }, + { url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" }, + { url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" }, + { url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" }, + { url = "https://files.pythonhosted.org/packages/77/cb/43692bcd5f7a0da6ec0ec6d58ee7cddb606d055ce94a62ac9b1aa481e969/greenlet-3.3.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c024b1e5696626890038e34f76140ed1daf858e37496d33f2af57f06189e70d7", size = 622297, upload-time = "2025-12-04T15:07:13.552Z" }, + { url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885, upload-time = "2025-12-04T14:26:02.368Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424, upload-time = "2025-12-04T15:04:23.757Z" }, + { url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017, upload-time = "2025-12-04T14:27:29.688Z" }, + { url = "https://files.pythonhosted.org/packages/6c/79/3912a94cf27ec503e51ba493692d6db1e3cd8ac7ac52b0b47c8e33d7f4f9/greenlet-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7a34b13d43a6b78abf828a6d0e87d3385680eaf830cd60d20d52f249faabf39", size = 301964, upload-time = "2025-12-04T14:36:58.316Z" }, + { url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" }, + { url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" }, + { url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311, upload-time = "2025-12-04T15:07:14.697Z" }, + { url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" }, + { url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" }, + { url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833, upload-time = "2025-12-04T14:32:23.929Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" }, + { url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" }, + { url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388, upload-time = "2025-12-04T15:07:15.789Z" }, + { url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" }, + { url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/9030e6f9aa8fd7808e9c31ba4c38f87c4f8ec324ee67431d181fe396d705/greenlet-3.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:73f51dd0e0bdb596fb0417e475fa3c5e32d4c83638296e560086b8d7da7c4170", size = 305387, upload-time = "2025-12-04T14:26:51.063Z" }, + { url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" }, + { url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" }, + { url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506, upload-time = "2025-12-04T15:07:16.906Z" }, + { url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/54/db/160dffb57ed9a3705c4cbcbff0ac03bdae45f1ca7d58ab74645550df3fbd/pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf", size = 2107999, upload-time = "2025-11-04T13:42:03.885Z" }, + { url = "https://files.pythonhosted.org/packages/a3/7d/88e7de946f60d9263cc84819f32513520b85c0f8322f9b8f6e4afc938383/pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5", size = 1929745, upload-time = "2025-11-04T13:42:06.075Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c2/aef51e5b283780e85e99ff19db0f05842d2d4a8a8cd15e63b0280029b08f/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d", size = 1920220, upload-time = "2025-11-04T13:42:08.457Z" }, + { url = "https://files.pythonhosted.org/packages/c7/97/492ab10f9ac8695cd76b2fdb24e9e61f394051df71594e9bcc891c9f586e/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60", size = 2067296, upload-time = "2025-11-04T13:42:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/ec/23/984149650e5269c59a2a4c41d234a9570adc68ab29981825cfaf4cfad8f4/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82", size = 2231548, upload-time = "2025-11-04T13:42:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/71/0c/85bcbb885b9732c28bec67a222dbed5ed2d77baee1f8bba2002e8cd00c5c/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5", size = 2362571, upload-time = "2025-11-04T13:42:16.208Z" }, + { url = "https://files.pythonhosted.org/packages/c0/4a/412d2048be12c334003e9b823a3fa3d038e46cc2d64dd8aab50b31b65499/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3", size = 2068175, upload-time = "2025-11-04T13:42:18.911Z" }, + { url = "https://files.pythonhosted.org/packages/73/f4/c58b6a776b502d0a5540ad02e232514285513572060f0d78f7832ca3c98b/pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425", size = 2177203, upload-time = "2025-11-04T13:42:22.578Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ae/f06ea4c7e7a9eead3d165e7623cd2ea0cb788e277e4f935af63fc98fa4e6/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504", size = 2148191, upload-time = "2025-11-04T13:42:24.89Z" }, + { url = "https://files.pythonhosted.org/packages/c1/57/25a11dcdc656bf5f8b05902c3c2934ac3ea296257cc4a3f79a6319e61856/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5", size = 2343907, upload-time = "2025-11-04T13:42:27.683Z" }, + { url = "https://files.pythonhosted.org/packages/96/82/e33d5f4933d7a03327c0c43c65d575e5919d4974ffc026bc917a5f7b9f61/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3", size = 2322174, upload-time = "2025-11-04T13:42:30.776Z" }, + { url = "https://files.pythonhosted.org/packages/81/45/4091be67ce9f469e81656f880f3506f6a5624121ec5eb3eab37d7581897d/pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460", size = 1990353, upload-time = "2025-11-04T13:42:33.111Z" }, + { url = "https://files.pythonhosted.org/packages/44/8a/a98aede18db6e9cd5d66bcacd8a409fcf8134204cdede2e7de35c5a2c5ef/pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b", size = 2015698, upload-time = "2025-11-04T13:42:35.484Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.45" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", version = "3.2.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.10' and platform_machine == 'AMD64') or (python_full_version < '3.10' and platform_machine == 'WIN32') or (python_full_version < '3.10' and platform_machine == 'aarch64') or (python_full_version < '3.10' and platform_machine == 'amd64') or (python_full_version < '3.10' and platform_machine == 'ppc64le') or (python_full_version < '3.10' and platform_machine == 'win32') or (python_full_version < '3.10' and platform_machine == 'x86_64')" }, + { name = "greenlet", version = "3.3.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.10' and platform_machine == 'AMD64') or (python_full_version >= '3.10' and platform_machine == 'WIN32') or (python_full_version >= '3.10' and platform_machine == 'aarch64') or (python_full_version >= '3.10' and platform_machine == 'amd64') or (python_full_version >= '3.10' and platform_machine == 'ppc64le') or (python_full_version >= '3.10' and platform_machine == 'win32') or (python_full_version >= '3.10' and platform_machine == 'x86_64')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/f9/5e4491e5ccf42f5d9cfc663741d261b3e6e1683ae7812114e7636409fcc6/sqlalchemy-2.0.45.tar.gz", hash = "sha256:1632a4bda8d2d25703fdad6363058d882541bdaaee0e5e3ddfa0cd3229efce88", size = 9869912, upload-time = "2025-12-09T21:05:16.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/70/75b1387d72e2847220441166c5eb4e9846dd753895208c13e6d66523b2d9/sqlalchemy-2.0.45-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c64772786d9eee72d4d3784c28f0a636af5b0a29f3fe26ff11f55efe90c0bd85", size = 2154148, upload-time = "2025-12-10T20:03:21.023Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a4/7805e02323c49cb9d1ae5cd4913b28c97103079765f520043f914fca4cb3/sqlalchemy-2.0.45-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7ae64ebf7657395824a19bca98ab10eb9a3ecb026bf09524014f1bb81cb598d4", size = 3233051, upload-time = "2025-12-09T22:06:04.768Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ec/32ae09139f61bef3de3142e85c47abdee8db9a55af2bb438da54a4549263/sqlalchemy-2.0.45-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f02325709d1b1a1489f23a39b318e175a171497374149eae74d612634b234c0", size = 3232781, upload-time = "2025-12-09T22:09:54.435Z" }, + { url = "https://files.pythonhosted.org/packages/ad/bd/bf7b869b6f5585eac34222e1cf4405f4ba8c3b85dd6b1af5d4ce8bca695f/sqlalchemy-2.0.45-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d2c3684fca8a05f0ac1d9a21c1f4a266983a7ea9180efb80ffeb03861ecd01a0", size = 3182096, upload-time = "2025-12-09T22:06:06.169Z" }, + { url = "https://files.pythonhosted.org/packages/21/6a/c219720a241bb8f35c88815ccc27761f5af7fdef04b987b0e8a2c1a6dcaa/sqlalchemy-2.0.45-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040f6f0545b3b7da6b9317fc3e922c9a98fc7243b2a1b39f78390fc0942f7826", size = 3205109, upload-time = "2025-12-09T22:09:55.969Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c4/6ccf31b2bc925d5d95fab403ffd50d20d7c82b858cf1a4855664ca054dce/sqlalchemy-2.0.45-cp310-cp310-win32.whl", hash = "sha256:830d434d609fe7bfa47c425c445a8b37929f140a7a44cdaf77f6d34df3a7296a", size = 2114240, upload-time = "2025-12-09T21:29:54.007Z" }, + { url = "https://files.pythonhosted.org/packages/de/29/a27a31fca07316def418db6f7c70ab14010506616a2decef1906050a0587/sqlalchemy-2.0.45-cp310-cp310-win_amd64.whl", hash = "sha256:0209d9753671b0da74da2cfbb9ecf9c02f72a759e4b018b3ab35f244c91842c7", size = 2137615, upload-time = "2025-12-09T21:29:55.85Z" }, + { url = "https://files.pythonhosted.org/packages/a2/1c/769552a9d840065137272ebe86ffbb0bc92b0f1e0a68ee5266a225f8cd7b/sqlalchemy-2.0.45-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e90a344c644a4fa871eb01809c32096487928bd2038bf10f3e4515cb688cc56", size = 2153860, upload-time = "2025-12-10T20:03:23.843Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f8/9be54ff620e5b796ca7b44670ef58bc678095d51b0e89d6e3102ea468216/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8c8b41b97fba5f62349aa285654230296829672fc9939cd7f35aab246d1c08b", size = 3309379, upload-time = "2025-12-09T22:06:07.461Z" }, + { url = "https://files.pythonhosted.org/packages/f6/2b/60ce3ee7a5ae172bfcd419ce23259bb874d2cddd44f67c5df3760a1e22f9/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12c694ed6468333a090d2f60950e4250b928f457e4962389553d6ba5fe9951ac", size = 3309948, upload-time = "2025-12-09T22:09:57.643Z" }, + { url = "https://files.pythonhosted.org/packages/a3/42/bac8d393f5db550e4e466d03d16daaafd2bad1f74e48c12673fb499a7fc1/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f7d27a1d977a1cfef38a0e2e1ca86f09c4212666ce34e6ae542f3ed0a33bc606", size = 3261239, upload-time = "2025-12-09T22:06:08.879Z" }, + { url = "https://files.pythonhosted.org/packages/6f/12/43dc70a0528c59842b04ea1c1ed176f072a9b383190eb015384dd102fb19/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d62e47f5d8a50099b17e2bfc1b0c7d7ecd8ba6b46b1507b58cc4f05eefc3bb1c", size = 3284065, upload-time = "2025-12-09T22:09:59.454Z" }, + { url = "https://files.pythonhosted.org/packages/cf/9c/563049cf761d9a2ec7bc489f7879e9d94e7b590496bea5bbee9ed7b4cc32/sqlalchemy-2.0.45-cp311-cp311-win32.whl", hash = "sha256:3c5f76216e7b85770d5bb5130ddd11ee89f4d52b11783674a662c7dd57018177", size = 2113480, upload-time = "2025-12-09T21:29:57.03Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fa/09d0a11fe9f15c7fa5c7f0dd26be3d235b0c0cbf2f9544f43bc42efc8a24/sqlalchemy-2.0.45-cp311-cp311-win_amd64.whl", hash = "sha256:a15b98adb7f277316f2c276c090259129ee4afca783495e212048daf846654b2", size = 2138407, upload-time = "2025-12-09T21:29:58.556Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c7/1900b56ce19bff1c26f39a4ce427faec7716c81ac792bfac8b6a9f3dca93/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3ee2aac15169fb0d45822983631466d60b762085bc4535cd39e66bea362df5f", size = 3333760, upload-time = "2025-12-09T22:11:02.66Z" }, + { url = "https://files.pythonhosted.org/packages/0a/93/3be94d96bb442d0d9a60e55a6bb6e0958dd3457751c6f8502e56ef95fed0/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba547ac0b361ab4f1608afbc8432db669bd0819b3e12e29fb5fa9529a8bba81d", size = 3348268, upload-time = "2025-12-09T22:13:49.054Z" }, + { url = "https://files.pythonhosted.org/packages/48/4b/f88ded696e61513595e4a9778f9d3f2bf7332cce4eb0c7cedaabddd6687b/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215f0528b914e5c75ef2559f69dca86878a3beeb0c1be7279d77f18e8d180ed4", size = 3278144, upload-time = "2025-12-09T22:11:04.14Z" }, + { url = "https://files.pythonhosted.org/packages/ed/6a/310ecb5657221f3e1bd5288ed83aa554923fb5da48d760a9f7622afeb065/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:107029bf4f43d076d4011f1afb74f7c3e2ea029ec82eb23d8527d5e909e97aa6", size = 3313907, upload-time = "2025-12-09T22:13:50.598Z" }, + { url = "https://files.pythonhosted.org/packages/5c/39/69c0b4051079addd57c84a5bfb34920d87456dd4c90cf7ee0df6efafc8ff/sqlalchemy-2.0.45-cp312-cp312-win32.whl", hash = "sha256:0c9f6ada57b58420a2c0277ff853abe40b9e9449f8d7d231763c6bc30f5c4953", size = 2112182, upload-time = "2025-12-09T21:39:30.824Z" }, + { url = "https://files.pythonhosted.org/packages/f7/4e/510db49dd89fc3a6e994bee51848c94c48c4a00dc905e8d0133c251f41a7/sqlalchemy-2.0.45-cp312-cp312-win_amd64.whl", hash = "sha256:8defe5737c6d2179c7997242d6473587c3beb52e557f5ef0187277009f73e5e1", size = 2139200, upload-time = "2025-12-09T21:39:32.321Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c8/7cc5221b47a54edc72a0140a1efa56e0a2730eefa4058d7ed0b4c4357ff8/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe187fc31a54d7fd90352f34e8c008cf3ad5d064d08fedd3de2e8df83eb4a1cf", size = 3277082, upload-time = "2025-12-09T22:11:06.167Z" }, + { url = "https://files.pythonhosted.org/packages/0e/50/80a8d080ac7d3d321e5e5d420c9a522b0aa770ec7013ea91f9a8b7d36e4a/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:672c45cae53ba88e0dad74b9027dddd09ef6f441e927786b05bec75d949fbb2e", size = 3293131, upload-time = "2025-12-09T22:13:52.626Z" }, + { url = "https://files.pythonhosted.org/packages/da/4c/13dab31266fc9904f7609a5dc308a2432a066141d65b857760c3bef97e69/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:470daea2c1ce73910f08caf10575676a37159a6d16c4da33d0033546bddebc9b", size = 3225389, upload-time = "2025-12-09T22:11:08.093Z" }, + { url = "https://files.pythonhosted.org/packages/74/04/891b5c2e9f83589de202e7abaf24cd4e4fa59e1837d64d528829ad6cc107/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9c6378449e0940476577047150fd09e242529b761dc887c9808a9a937fe990c8", size = 3266054, upload-time = "2025-12-09T22:13:54.262Z" }, + { url = "https://files.pythonhosted.org/packages/f1/24/fc59e7f71b0948cdd4cff7a286210e86b0443ef1d18a23b0d83b87e4b1f7/sqlalchemy-2.0.45-cp313-cp313-win32.whl", hash = "sha256:4b6bec67ca45bc166c8729910bd2a87f1c0407ee955df110d78948f5b5827e8a", size = 2110299, upload-time = "2025-12-09T21:39:33.486Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c5/d17113020b2d43073412aeca09b60d2009442420372123b8d49cc253f8b8/sqlalchemy-2.0.45-cp313-cp313-win_amd64.whl", hash = "sha256:afbf47dc4de31fa38fd491f3705cac5307d21d4bb828a4f020ee59af412744ee", size = 2136264, upload-time = "2025-12-09T21:39:36.801Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8d/bb40a5d10e7a5f2195f235c0b2f2c79b0bf6e8f00c0c223130a4fbd2db09/sqlalchemy-2.0.45-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d7009f40ce619d483d26ac1b757dfe3167b39921379a8bd1b596cf02dab4a6", size = 3521998, upload-time = "2025-12-09T22:13:28.622Z" }, + { url = "https://files.pythonhosted.org/packages/75/a5/346128b0464886f036c039ea287b7332a410aa2d3fb0bb5d404cb8861635/sqlalchemy-2.0.45-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d8a2ca754e5415cde2b656c27900b19d50ba076aa05ce66e2207623d3fe41f5a", size = 3473434, upload-time = "2025-12-09T22:13:30.188Z" }, + { url = "https://files.pythonhosted.org/packages/cc/64/4e1913772646b060b025d3fc52ce91a58967fe58957df32b455de5a12b4f/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f46ec744e7f51275582e6a24326e10c49fbdd3fc99103e01376841213028774", size = 3272404, upload-time = "2025-12-09T22:11:09.662Z" }, + { url = "https://files.pythonhosted.org/packages/b3/27/caf606ee924282fe4747ee4fd454b335a72a6e018f97eab5ff7f28199e16/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:883c600c345123c033c2f6caca18def08f1f7f4c3ebeb591a63b6fceffc95cce", size = 3277057, upload-time = "2025-12-09T22:13:56.213Z" }, + { url = "https://files.pythonhosted.org/packages/85/d0/3d64218c9724e91f3d1574d12eb7ff8f19f937643815d8daf792046d88ab/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2c0b74aa79e2deade948fe8593654c8ef4228c44ba862bb7c9585c8e0db90f33", size = 3222279, upload-time = "2025-12-09T22:11:11.1Z" }, + { url = "https://files.pythonhosted.org/packages/24/10/dd7688a81c5bc7690c2a3764d55a238c524cd1a5a19487928844cb247695/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a420169cef179d4c9064365f42d779f1e5895ad26ca0c8b4c0233920973db74", size = 3244508, upload-time = "2025-12-09T22:13:57.932Z" }, + { url = "https://files.pythonhosted.org/packages/aa/41/db75756ca49f777e029968d9c9fee338c7907c563267740c6d310a8e3f60/sqlalchemy-2.0.45-cp314-cp314-win32.whl", hash = "sha256:e50dcb81a5dfe4b7b4a4aa8f338116d127cb209559124f3694c70d6cd072b68f", size = 2113204, upload-time = "2025-12-09T21:39:38.365Z" }, + { url = "https://files.pythonhosted.org/packages/89/a2/0e1590e9adb292b1d576dbcf67ff7df8cf55e56e78d2c927686d01080f4b/sqlalchemy-2.0.45-cp314-cp314-win_amd64.whl", hash = "sha256:4748601c8ea959e37e03d13dcda4a44837afcd1b21338e637f7c935b8da06177", size = 2138785, upload-time = "2025-12-09T21:39:39.503Z" }, + { url = "https://files.pythonhosted.org/packages/42/39/f05f0ed54d451156bbed0e23eb0516bcad7cbb9f18b3bf219c786371b3f0/sqlalchemy-2.0.45-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd337d3526ec5298f67d6a30bbbe4ed7e5e68862f0bf6dd21d289f8d37b7d60b", size = 3522029, upload-time = "2025-12-09T22:13:32.09Z" }, + { url = "https://files.pythonhosted.org/packages/54/0f/d15398b98b65c2bce288d5ee3f7d0a81f77ab89d9456994d5c7cc8b2a9db/sqlalchemy-2.0.45-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9a62b446b7d86a3909abbcd1cd3cc550a832f99c2bc37c5b22e1925438b9367b", size = 3475142, upload-time = "2025-12-09T22:13:33.739Z" }, + { url = "https://files.pythonhosted.org/packages/53/01/a01b9829d146ba59972e6dfc88138142f5ffa4110e492c83326e7d765a17/sqlalchemy-2.0.45-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d29b2b99d527dbc66dd87c3c3248a5dd789d974a507f4653c969999fc7c1191b", size = 2157179, upload-time = "2025-12-10T20:05:13.998Z" }, + { url = "https://files.pythonhosted.org/packages/1f/78/ed43ed8ac27844f129adfc45a8735bab5dcad3e5211f4dc1bd7e676bc3ed/sqlalchemy-2.0.45-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:59a8b8bd9c6bedf81ad07c8bd5543eedca55fe9b8780b2b628d495ba55f8db1e", size = 3233038, upload-time = "2025-12-09T22:06:55.42Z" }, + { url = "https://files.pythonhosted.org/packages/24/1c/721ec797f21431c905ad98cbce66430d72a340935e3b7e3232cf05e015cc/sqlalchemy-2.0.45-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd93c6f5d65f254ceabe97548c709e073d6da9883343adaa51bf1a913ce93f8e", size = 3233117, upload-time = "2025-12-09T22:10:03.143Z" }, + { url = "https://files.pythonhosted.org/packages/52/33/dcfb8dffb2ccd7c6803d63454dc1917ef5ec5b5e281fecbbc0ed1de1f125/sqlalchemy-2.0.45-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6d0beadc2535157070c9c17ecf25ecec31e13c229a8f69196d7590bde8082bf1", size = 3182306, upload-time = "2025-12-09T22:06:56.894Z" }, + { url = "https://files.pythonhosted.org/packages/53/76/7cf8ce9e6dcac1d37125425aadec406d8a839dffc1b8763f6e7a56b0bf33/sqlalchemy-2.0.45-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e057f928ffe9c9b246a55b469c133b98a426297e1772ad24ce9f0c47d123bd5b", size = 3205587, upload-time = "2025-12-09T22:10:04.812Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ac/5cd0d14f7830981c06f468507237b0a8205691d626492b5551a67535eb30/sqlalchemy-2.0.45-cp39-cp39-win32.whl", hash = "sha256:c1c2091b1489435ff85728fafeb990f073e64f6f5e81d5cd53059773e8521eb6", size = 2115932, upload-time = "2025-12-09T22:09:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/b9/eb/76f6db8828c6e0cfac89820a07a40a2bab25e82e69827177b942a9bff42a/sqlalchemy-2.0.45-cp39-cp39-win_amd64.whl", hash = "sha256:56ead1f8dfb91a54a28cd1d072c74b3d635bcffbd25e50786533b822d4f2cde2", size = 2139570, upload-time = "2025-12-09T22:09:18.545Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-py3-none-any.whl", hash = "sha256:5225a288e4c8cc2308dbdd874edad6e7d0fd38eac1e9e5f23503425c8eee20d0", size = 1936672, upload-time = "2025-12-09T21:54:52.608Z" }, +] + +[[package]] +name = "sqlmodel" +source = { editable = "." } +dependencies = [ + { name = "pydantic" }, + { name = "sqlalchemy" }, +] + +[package.metadata] +requires-dist = [ + { name = "pydantic", specifier = ">=2.7.0" }, + { name = "sqlalchemy", specifier = ">=2.0.14,<2.1.0" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] From 0b756b562898d228d12aeea2fce70b8141aad8e6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 28 Dec 2025 12:32:46 +0000 Subject: [PATCH 901/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index ad044a98a8..f8a89cbe7a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Breaking Changes + +* ➖ Drop support for Pydantic v1. PR [#1701](https://github.com/fastapi/sqlmodel/pull/1701) by [@tiangolo](https://github.com/tiangolo). + ### Internal * ⬆ Bump dirty-equals from 0.9.0 to 0.11. PR [#1649](https://github.com/fastapi/sqlmodel/pull/1649) by [@dependabot[bot]](https://github.com/apps/dependabot). From 2cc11dada3f8f988d5f220a228ac23d60edfb79e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 28 Dec 2025 13:33:38 +0100 Subject: [PATCH 902/906] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.0.?= =?UTF-8?q?31?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-notes.md | 2 ++ sqlmodel/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index f8a89cbe7a..6e9f9de9e3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest Changes +## 0.0.31 + ### Breaking Changes * ➖ Drop support for Pydantic v1. PR [#1701](https://github.com/fastapi/sqlmodel/pull/1701) by [@tiangolo](https://github.com/tiangolo). diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index ad21f1c666..d4210b06d8 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.0.30" +__version__ = "0.0.31" # Re-export from SQLAlchemy from sqlalchemy.engine import create_engine as create_engine From 8dbcd643d316da652026eaa2143f736184e8b071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 28 Dec 2025 05:08:19 -0800 Subject: [PATCH 903/906] =?UTF-8?q?=E2=9C=85=20Update=20tests,=20remove=20?= =?UTF-8?q?conditionals=20for=20Pydantic=20v1=20(#1702)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_delete/test_tutorial001.py | 71 ++----- .../test_limit_and_offset/test_tutorial001.py | 29 +-- .../test_multiple_models/test_tutorial001.py | 29 +-- .../test_multiple_models/test_tutorial002.py | 29 +-- .../test_read_one/test_tutorial001.py | 29 +-- .../test_relationships/test_tutorial001.py | 201 +++++------------- .../test_response_model/test_tutorial001.py | 29 +-- .../test_tutorial001.py | 71 ++----- .../test_simple_hero_api/test_tutorial001.py | 29 +-- .../test_teams/test_tutorial001.py | 141 ++++-------- .../test_update/test_tutorial001.py | 71 ++----- .../test_update/test_tutorial002.py | 85 +++----- 12 files changed, 230 insertions(+), 584 deletions(-) diff --git a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py index ffbf7587f1..a766b37d47 100644 --- a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py @@ -2,7 +2,6 @@ from types import ModuleType import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -300,16 +299,10 @@ def test_tutorial(module: ModuleType): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "HeroPublic": { @@ -319,16 +312,10 @@ def test_tutorial(module: ModuleType): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, "id": {"title": "Id", "type": "integer"}, }, }, @@ -336,36 +323,18 @@ def test_tutorial(module: ModuleType): "title": "HeroUpdate", "type": "object", "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "title": "Secret Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "secret_name": { + "title": "Secret Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py index cbc722c5c2..241241a422 100644 --- a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py @@ -2,7 +2,6 @@ from types import ModuleType import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -235,16 +234,10 @@ def test_tutorial(module: ModuleType): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "HeroPublic": { @@ -254,16 +247,10 @@ def test_tutorial(module: ModuleType): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, "id": {"title": "Id", "type": "integer"}, }, }, diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py index d70dce32e6..1690ba26ef 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py @@ -2,7 +2,6 @@ from types import ModuleType import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlalchemy import inspect from sqlalchemy.engine.reflection import Inspector @@ -159,16 +158,10 @@ def test_tutorial(module: ModuleType): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "HeroPublic": { @@ -179,16 +172,10 @@ def test_tutorial(module: ModuleType): "id": {"title": "Id", "type": "integer"}, "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py index 8d7c8064e7..b17fc20ffa 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py @@ -2,7 +2,6 @@ from types import ModuleType import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlalchemy import inspect from sqlalchemy.engine.reflection import Inspector @@ -159,16 +158,10 @@ def test_tutorial(module: ModuleType): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "HeroPublic": { @@ -178,16 +171,10 @@ def test_tutorial(module: ModuleType): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, "id": {"title": "Id", "type": "integer"}, }, }, diff --git a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py index 7ebbecd8de..420da4a379 100644 --- a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py @@ -2,7 +2,6 @@ from types import ModuleType import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -178,16 +177,10 @@ def test_tutorial(module: ModuleType): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "HeroPublic": { @@ -197,16 +190,10 @@ def test_tutorial(module: ModuleType): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, "id": {"title": "Id", "type": "integer"}, }, }, diff --git a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py index cf24466c13..07be8c3089 100644 --- a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py @@ -2,7 +2,6 @@ from types import ModuleType import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -549,26 +548,14 @@ def test_tutorial(module: ModuleType): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, + "team_id": { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "HeroPublic": { @@ -578,26 +565,14 @@ def test_tutorial(module: ModuleType): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, + "team_id": { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, "id": {"title": "Id", "type": "integer"}, }, }, @@ -608,85 +583,43 @@ def test_tutorial(module: ModuleType): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, + "team_id": { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, "id": {"title": "Id", "type": "integer"}, - "team": IsDict( - { - "anyOf": [ - {"$ref": "#/components/schemas/TeamPublic"}, - {"type": "null"}, - ] - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"$ref": "#/components/schemas/TeamPublic"} - ), + "team": { + "anyOf": [ + {"$ref": "#/components/schemas/TeamPublic"}, + {"type": "null"}, + ] + }, }, }, "HeroUpdate": { "title": "HeroUpdate", "type": "object", "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "title": "Secret Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "secret_name": { + "title": "Secret Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, + "team_id": { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "TeamCreate": { @@ -728,36 +661,18 @@ def test_tutorial(module: ModuleType): "title": "TeamUpdate", "type": "object", "properties": { - "id": IsDict( - { - "title": "Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Id", "type": "integer"} - ), - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "headquarters": IsDict( - { - "title": "Headquarters", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Headquarters", "type": "string"} - ), + "id": { + "title": "Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "headquarters": { + "title": "Headquarters", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py index 1ad004e939..2951268d99 100644 --- a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py @@ -2,7 +2,6 @@ from types import ModuleType import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -131,28 +130,16 @@ def test_tutorial(module: ModuleType): "required": ["name", "secret_name"], "type": "object", "properties": { - "id": IsDict( - { - "title": "Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Id", "type": "integer"} - ), + "id": { + "title": "Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py index 6e441cccbc..6528f85333 100644 --- a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py @@ -2,7 +2,6 @@ from types import ModuleType import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -302,16 +301,10 @@ def test_tutorial(module: ModuleType): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "HeroPublic": { @@ -321,16 +314,10 @@ def test_tutorial(module: ModuleType): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, "id": {"title": "Id", "type": "integer"}, }, }, @@ -338,36 +325,18 @@ def test_tutorial(module: ModuleType): "title": "HeroUpdate", "type": "object", "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "title": "Secret Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "secret_name": { + "title": "Secret Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py index b803cee28a..b6f88b136e 100644 --- a/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py @@ -2,7 +2,6 @@ from types import ModuleType import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -137,28 +136,16 @@ def test_tutorial(module: ModuleType): "required": ["name", "secret_name"], "type": "object", "properties": { - "id": IsDict( - { - "title": "Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Id", "type": "integer"} - ), + "id": { + "title": "Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py index 3b028b7d91..6982ee9913 100644 --- a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py @@ -2,7 +2,6 @@ from types import ModuleType import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -534,26 +533,14 @@ def test_tutorial(module: ModuleType): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, + "team_id": { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "HeroPublic": { @@ -563,26 +550,14 @@ def test_tutorial(module: ModuleType): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, + "team_id": { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, "id": {"title": "Id", "type": "integer"}, }, }, @@ -590,46 +565,22 @@ def test_tutorial(module: ModuleType): "title": "HeroUpdate", "type": "object", "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "title": "Secret Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "team_id": IsDict( - { - "title": "Team Id", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Team Id", "type": "integer"} - ), + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "secret_name": { + "title": "Secret Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, + "team_id": { + "title": "Team Id", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "TeamCreate": { @@ -655,26 +606,14 @@ def test_tutorial(module: ModuleType): "title": "TeamUpdate", "type": "object", "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "headquarters": IsDict( - { - "title": "Headquarters", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Headquarters", "type": "string"} - ), + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "headquarters": { + "title": "Headquarters", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py index 9bc54fbcfb..cba5c1ae62 100644 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py @@ -2,7 +2,6 @@ from types import ModuleType import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import create_engine from sqlmodel.pool import StaticPool @@ -279,16 +278,10 @@ def test_tutorial(module: ModuleType): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "HeroPublic": { @@ -298,16 +291,10 @@ def test_tutorial(module: ModuleType): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, "id": {"title": "Id", "type": "integer"}, }, }, @@ -315,36 +302,18 @@ def test_tutorial(module: ModuleType): "title": "HeroUpdate", "type": "object", "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "title": "Secret Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "title": "Age", - "anyOf": [{"type": "integer"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "secret_name": { + "title": "Secret Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "age": { + "title": "Age", + "anyOf": [{"type": "integer"}, {"type": "null"}], + }, }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial002.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial002.py index ede67fe4e4..22110ecb03 100644 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial002.py +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial002.py @@ -2,7 +2,6 @@ from types import ModuleType import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient from sqlmodel import Session, create_engine from sqlmodel.pool import StaticPool @@ -342,16 +341,10 @@ def test_tutorial(module: ModuleType): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Age", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Age", + }, "password": {"type": "string", "title": "Password"}, }, }, @@ -362,16 +355,10 @@ def test_tutorial(module: ModuleType): "properties": { "name": {"title": "Name", "type": "string"}, "secret_name": {"title": "Secret Name", "type": "string"}, - "age": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Age", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), + "age": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Age", + }, "id": {"title": "Id", "type": "integer"}, }, }, @@ -379,46 +366,22 @@ def test_tutorial(module: ModuleType): "title": "HeroUpdate", "type": "object", "properties": { - "name": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Name", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "secret_name": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Secret Name", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Secret Name", "type": "string"} - ), - "age": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": "Age", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Age", "type": "integer"} - ), - "password": IsDict( - { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Password", - } - ) - | IsDict( - # TODO: Remove when deprecating Pydantic v1 - {"title": "Password", "type": "string"} - ), + "name": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Name", + }, + "secret_name": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Secret Name", + }, + "age": { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Age", + }, + "password": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Password", + }, }, }, "ValidationError": { From 401aab90457176bac7b98e7d2c3dcb8ae0801a95 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 28 Dec 2025 13:08:40 +0000 Subject: [PATCH 904/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 6e9f9de9e3..5c3d02cea3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,10 @@ ## Latest Changes +### Internal + +* ✅ Update tests, remove conditionals for Pydantic v1. PR [#1702](https://github.com/fastapi/sqlmodel/pull/1702) by [@tiangolo](https://github.com/tiangolo). + ## 0.0.31 ### Breaking Changes From 2f77f306379f2a857768fef3d0c6bd2c7724cc9f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Dec 2025 16:02:54 +0100 Subject: [PATCH 905/906] =?UTF-8?q?=E2=AC=86=20Update=20fastapi=20requirem?= =?UTF-8?q?ent=20from=20>=3D0.103.2,<0.126.0=20to=20>=3D0.103.2,<0.129.0?= =?UTF-8?q?=20(#1703)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⬆ Update fastapi requirement Updates the requirements on [fastapi](https://github.com/fastapi/fastapi) to permit the latest version. - [Release notes](https://github.com/fastapi/fastapi/releases) - [Commits](https://github.com/fastapi/fastapi/compare/0.103.2...0.128.0) --- updated-dependencies: - dependency-name: fastapi dependency-version: 0.128.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 82d1f8480c..b61b8429e6 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -5,7 +5,7 @@ coverage[toml] >=6.2,<8.0 mypy ==1.19.1 ruff ==0.14.10 # For FastAPI tests -fastapi >=0.103.2,<0.126.0 +fastapi >=0.103.2,<0.129.0 httpx ==0.28.1 dirty-equals ==0.11 jinja2 ==3.1.6 From 5c2dbe419edc2d15200eee5269c9508987944ed8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 30 Dec 2025 15:03:11 +0000 Subject: [PATCH 906/906] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 5c3d02cea3..84790f01c6 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ### Internal +* ⬆ Update fastapi requirement from >=0.103.2,<0.126.0 to >=0.103.2,<0.129.0. PR [#1703](https://github.com/fastapi/sqlmodel/pull/1703) by [@dependabot[bot]](https://github.com/apps/dependabot). * ✅ Update tests, remove conditionals for Pydantic v1. PR [#1702](https://github.com/fastapi/sqlmodel/pull/1702) by [@tiangolo](https://github.com/tiangolo). ## 0.0.31