Skip to content

Commit

Permalink
Initial commit (#1)
Browse files Browse the repository at this point in the history
dtiesling authored Nov 22, 2023
1 parent 19cf762 commit 6172516
Showing 19 changed files with 2,819 additions and 1 deletion.
40 changes: 40 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Pypi Publish

on:
release:
types:
- created
env:
HATCH_INDEX_AUTH: ${{ secrets.HATCH_INDEX_AUTH }}
HATCH_INDEX_USER: ${{ secrets.HATCH_INDEX_USER }}

jobs:
publish:
name: Pypi Publish
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v1
with:
python-version: 3.9
- name: Extract Release Name
run: echo "RELEASE_NAME=$(echo $GITHUB_REF | sed -n 's/refs\/tags\///p')" >> $GITHUB_ENV
- name: Install hatch
run: pip install hatch
- name: Publish to Pypi
run: |
hatch version $RELEASE_NAME
hatch build
hatch publish -r test -n
- name: Checkout Main
run: |
git fetch
git stash
git checkout main
git stash apply
- name: Add & Commit
uses: EndBug/[email protected]
with:
add: 'src/flask_muck/__init__.py'

26 changes: 26 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: CI Testing

on:
pull_request:
branches:
- main
push:
branches:
- main

jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: psf/black@stable
- uses: actions/setup-python@v1
with:
python-version: 3.9
- run: pip install poetry
- run: poetry install --with dev
- run: poetry run mypy src/
- run: poetry run pytest --cov=flask_muck


6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)

# flask-muck
Batteries included framework for generating RESTful apis using Flask and SqlAlchemy.
Batteries included framework for generating RESTful APIs with built-in CRUD in a Flask/SqlAlchemy stack.

## [DOCUMENTATION COMING SOON, SEE EXAMPLES FOLDER TO GET STARTED]
19 changes: 19 additions & 0 deletions examples/simple_todo_api/Pipfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
flask = "*"
flask-sqlalchemy = "*"
pipenv = "*"
install = "*"
marshmallow = "*"
flask-muck = {file = "../.."}
flask-login = "*"
webargs = "*"

[dev-packages]

[requires]
python_version = "3.11"
311 changes: 311 additions & 0 deletions examples/simple_todo_api/Pipfile.lock

Large diffs are not rendered by default.

70 changes: 70 additions & 0 deletions examples/simple_todo_api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Simple ToDo App API Example

This is a simple example of a complete Flask app hosting a REST API. This example demonstrates the tech stack that
Flask-Muck sits in and how to set create and register the views.

Below are instructions are running the testing the example.

## Prequisites

- [Python 3.11](https://www.python.org/downloads/)
- [pipenv](https://pipenv.pypa.io/en/latest/#install-pipenv-today)

## Running The App

`pipenv run python3 app.py`

## CURL Commands

### Login
`curl -X POST --location "http://127.0.0.1:5000/api/v1/login"`

### Logout
`curl -X POST --location "http://127.0.0.1:5000/api/v1/logout"`

### Create a ToDo item
```
curl -X POST --location "http://127.0.0.1:5000/api/v1/todos" \
-H "Content-Type: application/json" \
-d "{
\"text\": \"take out garbage again\"
}"
```

### List all ToDo items (flat)
```
curl -X GET --location "http://127.0.0.1:5000/api/v1/todos" \
-d "Accept: application/json"
```

### List all ToDo items (paginated)
```
curl -X GET --location "http://127.0.0.1:5000/api/v1/todos?limit=2&offset=1" \
-d "Accept: application/json"
```

### Get ToDo item
```
curl -X GET --location "http://127.0.0.1:5000/api/v1/todos/1" \
-d "Accept: application/json"
```

### Update ToDo item
```
curl -X PUT --location "http://127.0.0.1:5000/api/v1/todos/1" \
-H "Content-Type: application/json" \
-d "{
\"text\": \"Updated todo item\"
}"
```

### Patch ToDo item
```
curl -X PATCH --location "http://127.0.0.1:5000/api/v1/todos/1" \
-H "Content-Type: application/json" \
-d "{
\"text\": \"Updated todo item\"
}"
```


113 changes: 113 additions & 0 deletions examples/simple_todo_api/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import marshmallow as ma
from flask import Flask, Blueprint
from flask_login import (
LoginManager,
login_required,
UserMixin,
login_user,
logout_user,
)
from flask_sqlalchemy import SQLAlchemy
from marshmallow import fields as mf
from sqlalchemy.orm import DeclarativeBase

from flask_muck.views import MuckApiView

# Create a Flask app
app = Flask(__name__)
app.config["SECRET_KEY"] = "super-secret"


# Init Flask-SQLAlchemy and set database to a local sqlite file.
class Base(DeclarativeBase):
pass


db = SQLAlchemy(model_class=Base)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///todo_example.db"
db.init_app(app)


# Create SQLAlchemy database models.
class UserModel(db.Model, UserMixin):
"""Flask-Login User model to use for authentication."""

id = db.Column(db.Integer, primary_key=True, autoincrement=True)


class TodoModel(db.Model):
"""Simple model to track ToDo items."""

id = db.Column(db.Integer, primary_key=True, autoincrement=True)
text = db.Column(db.String, nullable=False)


# Create Marshmallow schemas for serialization.
class TodoSchema(ma.Schema):
"""ToDo model schema that can be used for CRUD operations."""

id = mf.Integer(required=True, dump_only=True)
text = mf.String(required=True)


# Add a Flask blueprint for the base of the REST API and register it with the app.
api_blueprint = Blueprint("v1_api", __name__, url_prefix="/api/v1/")


# Init Flask-Login for user authentication and add login/logout endpoints.
login_manager = LoginManager()
login_manager.init_app(app)


@login_manager.user_loader
def load_user(user_id):
return UserModel.query.get(user_id)


@api_blueprint.route("login", methods=["POST"])
def login_view():
"""Dummy login view that creates a User and authenticates them."""
user = UserModel()
db.session.add(user)
db.session.commit()
login_user(user)
return {}, 200


@api_blueprint.route("logout", methods=["POST"])
def logout_view():
logout_user()
return {}, 200


# Add Muck views to generate CRUD REST API.
class BaseApiView(MuckApiView):
"""Base view to inherit from. Helpful for setting class variables shared with all API views such as "sqlalchemy_db"
and "decorators".
"""

session = db.session
decorators = [login_required]


class TodoApiView(BaseApiView):
"""ToDo API view that provides all RESTful CRUD operations."""

Model = TodoModel
ResponseSchema = TodoSchema
CreateSchema = TodoSchema
PatchSchema = TodoSchema
UpdateSchema = TodoSchema
searchable_columns = [TodoModel.text]


# Add all url rules to the blueprint.
TodoApiView.add_crud_to_blueprint(api_blueprint, url_prefix="todos")

# Register api blueprint with the app.
app.register_blueprint(api_blueprint)

if __name__ == "__main__":
with app.app_context():
db.create_all()
app.run(debug=True)
788 changes: 788 additions & 0 deletions poetry.lock

Large diffs are not rendered by default.

82 changes: 82 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "Flask-Muck"
dynamic = ["version"]
authors = [
{ name="Daniel Tiesling", email="tiesling@gmail.com" },
]
description = "Batteries included framework for generating RESTful apis using Flask and SqlAlchemy."
readme = "README.md"
requires-python = ">=3.9"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]

[project.urls]
"Homepage" = "https://github.com/dtiesling/flask-muck"

[tool]
[tool.hatch.version]
path = "src/flask_muck/__init__.py"

[tool.hatch.build.targets.sdist]
exclude = [
"/.github",
"/examples",
]

[tool.hatch.build.targets.wheel]
packages = ["src/flask_muck"]

[tool.poetry]
name = "flask-muck"
description = "Batteries included framework for generating RESTful apis using Flask and SqlAlchemy."
version = "0.0.1"
authors = ["Daniel Tiesling <tiesling@gmail.com>"]

[tool.poetry.dependencies]
python = "^3.9"
Flask = "^2.0.0"
sqlalchemy = "^2.0.23"
webargs = "^8.3.0"
types-requests = "^2.31.0.10"
marshmallow = "^3.20.1"

[tool.poetry.group.dev.dependencies]
mypy = "^1.6.1"
types-flask = "^1.1.6"
types-requests = "^2.31.0.10"
sqlalchemy-stubs = "^0.4"
pytest = "^7.4.3"
flask-login = "^0.6.3"
flask-sqlalchemy = "^3.1.1"
coverage = "^7.3.2"
pytest-cov = "^4.1.0"

[tool.mypy]
packages = "src"
strict = true
disallow_untyped_calls = false
warn_return_any = false
disallow_any_generics = false

[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = "test*.py"
pythonpath = ["src"]

[tool.coverage.run]
branch = true

[tool.coverage.report]
fail_under = 95
exclude_also = [
"@(abc\\.)?abstractmethod",
"if TYPE_CHECKING:",
"raise NotImplementedError"
]
4 changes: 4 additions & 0 deletions src/flask_muck/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .views import MuckApiView
from .callback import MuckCallback

VERSION = "0.0.2rc1"
19 changes: 19 additions & 0 deletions src/flask_muck/callback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from abc import ABC, abstractmethod
from enum import Enum

from flask_muck.types import SqlaModel, JsonDict


class CallbackType(Enum):
pre = "pre"
post = "post"


class MuckCallback(ABC):
def __init__(self, resource: SqlaModel, kwargs: JsonDict):
self.resource = resource
self.kwargs = kwargs

@abstractmethod
def execute(self) -> None:
...
2 changes: 2 additions & 0 deletions src/flask_muck/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class MuckImplementationError(Exception):
pass
8 changes: 8 additions & 0 deletions src/flask_muck/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from typing import Any, Union

from sqlalchemy.orm import DeclarativeBase # type: ignore

JsonDict = dict[str, Any]
ResourceId = Union[str, int]
SqlaModelType = type[DeclarativeBase]
SqlaModel = DeclarativeBase
67 changes: 67 additions & 0 deletions src/flask_muck/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from __future__ import annotations
from typing import Optional, TYPE_CHECKING, Union

from flask import request
from sqlalchemy import Column, inspect

from flask_muck.exceptions import MuckImplementationError
from flask_muck.types import SqlaModelType

if TYPE_CHECKING:
from flask_muck.views import MuckApiView


def get_url_rule(muck_view: type[MuckApiView], append_rule: Optional[str]) -> str:
"""Recursively build the url rule for a MuckApiView by looking at its parent if it exists."""
rule = muck_view.api_name
if append_rule:
rule = f"{rule}/{append_rule}"
if muck_view.parent:
rule = f"<{muck_view.parent.primary_key_type.__name__}:{muck_view.parent.api_name}_id>/{rule}"
return get_url_rule(muck_view.parent, rule)
if not rule.endswith("/"):
rule = rule + "/"
return rule


def get_fk_column(
parent_model: SqlaModelType, child_model: SqlaModelType
) -> Optional[Column]:
"""Get the foreign key column for a child model."""
for column in inspect(child_model).columns:
if column.foreign_keys:
for fk in column.foreign_keys:
if fk.column.table == parent_model.__table__:
return column
raise MuckImplementationError(
f"The {child_model.__name__} model does not have a foreign key to the {parent_model.__name__} model. "
f"Your MuckApiView parents are not configured correctly."
)


def get_query_filters_from_request_path(
view: Union[type[MuckApiView], MuckApiView], query_filters: list
) -> list:
"""Recursively builds query kwargs from the request path based on nested MuckApiViews. If the view has no parent
then nothing is done and original query_kwargs are returned.
"""
if view.parent:
child_model = view.Model
parent_model = view.parent.Model
fk_column = get_fk_column(parent_model, child_model)
query_filters.append(
fk_column == request.view_args[f"{view.parent.api_name}_id"]
)
return get_query_filters_from_request_path(view.parent, query_filters)
return query_filters


def get_join_models_from_parent_views(
view: Union[type[MuckApiView], MuckApiView], join_models: list[SqlaModelType]
) -> list[SqlaModelType]:
"""Recursively builds a list of models that need to be joined in queries based on the view's parents.."""
if view.parent:
join_models.append(view.parent.Model)
return get_join_models_from_parent_views(view.parent, join_models)
join_models.reverse()
return join_models
408 changes: 408 additions & 0 deletions src/flask_muck/views.py

Large diffs are not rendered by default.

Empty file added tests/__init__.py
Empty file.
189 changes: 189 additions & 0 deletions tests/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import marshmallow as ma
from flask import Flask, Blueprint
from flask_login import (
LoginManager,
login_required,
UserMixin,
login_user,
logout_user,
FlaskLoginClient,
)
from flask_sqlalchemy import SQLAlchemy
from marshmallow import fields as mf
from sqlalchemy.orm import DeclarativeBase, Mapped

from flask_muck import MuckCallback
from flask_muck.views import MuckApiView


login_manager = LoginManager()


# Init Flask-SQLAlchemy and set database to a local sqlite file.
class Base(DeclarativeBase):
pass


db = SQLAlchemy(model_class=Base)


# Create SQLAlchemy database models.
class UserModel(db.Model, UserMixin):
"""Flask-Login User model to use for authentication."""

id = db.Column(db.Integer, primary_key=True, autoincrement=True)


class FamilyModel(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
surname = db.Column(db.String, nullable=False)


class GuardianModel(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String, nullable=False, unique=True)
age = db.Column(db.Integer, nullable=True)
family_id = db.Column(db.Integer, db.ForeignKey(FamilyModel.id))
family = db.relationship(FamilyModel)
children: Mapped[list["ChildModel"]] = db.relationship()


class ChildModel(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String, nullable=False)
age = db.Column(db.Integer, nullable=True)
family_id = db.Column(db.Integer, db.ForeignKey(FamilyModel.id))
guardian_id = db.Column(db.Integer, db.ForeignKey(GuardianModel.id))
guardian = db.relationship(GuardianModel, back_populates="children")
toy: Mapped["ToyModel"] = db.relationship(uselist=False)


class ToyModel(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String, nullable=False)
family_id = db.Column(db.Integer, db.ForeignKey(FamilyModel.id))
child_id = db.Column(db.Integer, db.ForeignKey(ChildModel.id))
child = db.relationship(ChildModel, back_populates="toy")


class GuardianSchema(ma.Schema):
name = mf.String(required=True)


class ChildSchema(ma.Schema):
name = mf.String(required=True)
guardian_id = mf.Integer(required=True, load_only=True)


class GuardianDetailSchema(ma.Schema):
name = mf.String(required=True)
children = mf.Nested(ChildSchema, many=True)


class ToySchema(ma.Schema):
name = mf.String(required=True)
child_id = mf.Integer(required=True, load_only=True)


api_blueprint = Blueprint("api", __name__, url_prefix="/")


@login_manager.user_loader
def load_user(user_id):
return UserModel.query.get(user_id)


@api_blueprint.route("login", methods=["POST"])
def login_view():
"""Dummy login view that creates a User and authenticates them."""
user = UserModel()
db.session.add(user)
db.session.commit()
login_user(user)
return {}, 200


@api_blueprint.route("logout", methods=["POST"])
def logout_view():
logout_user()
return {}, 200


# Add Muck views to generate CRUD REST API.
class PreCallback(MuckCallback):
def execute(self) -> None:
return


class PostCallback(MuckCallback):
def execute(self) -> None:
return


class BaseApiView(MuckApiView):
"""Base view to inherit from. Helpful for setting class variables shared with all API views such as "sqlalchemy_db"
and "decorators".
"""

session = db.session
decorators = [login_required]
pre_create_callbacks = [PreCallback]
pre_update_callbacks = [PreCallback]
pre_patch_callbacks = [PreCallback]
pre_delete_callbacks = [PreCallback]
post_create_callbacks = [PostCallback]
post_update_callbacks = [PostCallback]
post_patch_callbacks = [PostCallback]
post_delete_callbacks = [PostCallback]


class GuardianApiView(BaseApiView):
api_name = "guardians"
Model = GuardianModel
ResponseSchema = GuardianSchema
CreateSchema = GuardianSchema
PatchSchema = GuardianSchema
UpdateSchema = GuardianSchema
DetailSchema = GuardianDetailSchema
searchable_columns = [GuardianModel.name, GuardianModel.age]


class ChildApiView(BaseApiView):
api_name = "children"
Model = ChildModel
ResponseSchema = ChildSchema
CreateSchema = ChildSchema
PatchSchema = ChildSchema
UpdateSchema = ChildSchema
parent = GuardianApiView
searchable_columns = [ChildModel.name]


class ToyApiView(BaseApiView):
api_name = "toy"
Model = ToyModel
ResponseSchema = ToySchema
CreateSchema = ToySchema
PatchSchema = ToySchema
UpdateSchema = ToySchema
parent = ChildApiView
one_to_one_api = True


# Add all url rules to the blueprint.
GuardianApiView.add_crud_to_blueprint(api_blueprint)
ChildApiView.add_crud_to_blueprint(api_blueprint)
ToyApiView.add_crud_to_blueprint(api_blueprint)


def create_app() -> Flask:
app = Flask(__name__)
app.config["SECRET_KEY"] = "super-secret"
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///todo_example.db"
app.config["TESTING"] = True
app.config["DEBUG"] = True
app.test_client_class = FlaskLoginClient
login_manager.init_app(app)
db.init_app(app)
app.register_blueprint(api_blueprint)
return app
249 changes: 249 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
from typing import Callable, Literal

import pytest
from flask import Flask
from flask.testing import FlaskClient
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import DeclarativeBase

from flask_muck.types import JsonDict
from tests.app import (
create_app,
UserModel,
GuardianModel,
ChildModel,
ToyModel,
FamilyModel,
)
from tests.app import db as _db


def make_request(
client: FlaskClient,
method: Literal["get", "put", "patch", "post", "delete"],
url: str,
expected_status_code: int,
**kwargs,
) -> JsonDict:
response = getattr(client, method)(url, **kwargs)
if response.status_code != expected_status_code:
raise AssertionError(
f"Expected status code {expected_status_code}, got {response.status_code}"
)
return response.json


@pytest.fixture
def create_model(db) -> Callable:
def _create_model(model_instance: DeclarativeBase) -> DeclarativeBase:
db.session.add(model_instance)
db.session.flush()
return model_instance

return _create_model


@pytest.fixture
def get(client) -> Callable:
def _get(url, expected_status_code=200, **kwargs):
return make_request(client, "get", url, expected_status_code, **kwargs)

return _get


@pytest.fixture
def post(client) -> Callable:
def _post(url, expected_status_code=201, **kwargs):
return make_request(client, "post", url, expected_status_code, **kwargs)

return _post


@pytest.fixture
def put(client) -> Callable:
def _put(url, expected_status_code=200, **kwargs):
return make_request(client, "put", url, expected_status_code, **kwargs)

return _put


@pytest.fixture
def patch(client) -> Callable:
def _patch(url, expected_status_code=200, **kwargs):
return make_request(client, "patch", url, expected_status_code, **kwargs)

return _patch


@pytest.fixture
def delete(client) -> Callable:
def _delete(url, expected_status_code=204, **kwargs):
return make_request(client, "delete", url, expected_status_code, **kwargs)

return _delete


@pytest.fixture
def db(app) -> SQLAlchemy:
_db.create_all()
yield _db
_db.session.close()
_db.drop_all()


@pytest.fixture(scope="session")
def app() -> Flask:
app = create_app()
with app.app_context():
yield app


@pytest.fixture
def client(app, user) -> FlaskClient:
return app.test_client(user=user)


@pytest.fixture
def user(create_model) -> UserModel:
return create_model(UserModel())


@pytest.fixture
def family(create_model) -> FamilyModel:
return create_model(FamilyModel(surname="Brown"))


@pytest.fixture
def guardian(family, create_model) -> GuardianModel:
return create_model(GuardianModel(name="Samantha", family_id=family.id))


@pytest.fixture
def child(family, guardian, create_model) -> ChildModel:
return create_model(
ChildModel(name="Tamara", family_id=family.id, guardian_id=guardian.id)
)


@pytest.fixture
def simpson_family(create_model) -> FamilyModel:
return create_model(FamilyModel(surname="Simpsons"))


@pytest.fixture
def marge(simpson_family, create_model) -> GuardianModel:
return create_model(
GuardianModel(name="Marge", age=34, family_id=simpson_family.id)
)


@pytest.fixture
def bart(simpson_family, marge, create_model) -> ChildModel:
return create_model(
ChildModel(
name="Bart", age=10, guardian_id=marge.id, family_id=simpson_family.id
)
)


@pytest.fixture
def maggie(simpson_family, marge, create_model) -> ChildModel:
return create_model(
ChildModel(
name="Maggie", age=1, guardian_id=marge.id, family_id=simpson_family.id
)
)


@pytest.fixture
def lisa(simpson_family, marge, create_model) -> ChildModel:
return create_model(
ChildModel(
name="Lisa", age=8, guardian_id=marge.id, family_id=simpson_family.id
)
)


@pytest.fixture
def skateboard(simpson_family, bart, create_model) -> ToyModel:
return create_model(
ToyModel(name="Skateboard", child_id=bart.id, family_id=simpson_family.id)
)


@pytest.fixture
def saxophone(simpson_family, lisa, create_model) -> ToyModel:
return create_model(
ToyModel(name="Saxophone", child_id=lisa.id, family_id=simpson_family.id)
)


@pytest.fixture
def pacifier(simpson_family, maggie, create_model) -> ToyModel:
return create_model(
ToyModel(name="Pacifier", child_id=maggie.id, family_id=simpson_family.id)
)


@pytest.fixture
def simpsons(marge, bart, maggie, lisa, skateboard, saxophone, pacifier) -> None:
pass


@pytest.fixture
def belcher_family(create_model) -> FamilyModel:
return create_model(FamilyModel(surname="Belcher"))


@pytest.fixture
def bob(belcher_family, create_model) -> GuardianModel:
return create_model(GuardianModel(name="Bob", age=46, family_id=belcher_family.id))


@pytest.fixture
def tina(belcher_family, bob, create_model) -> ChildModel:
return create_model(
ChildModel(name="Tina", age=12, guardian_id=bob.id, family_id=belcher_family.id)
)


@pytest.fixture
def louise(belcher_family, bob, create_model) -> ChildModel:
return create_model(
ChildModel(
name="Louise", age=9, guardian_id=bob.id, family_id=belcher_family.id
)
)


@pytest.fixture
def gene(belcher_family, bob, create_model) -> ChildModel:
return create_model(
ChildModel(name="Gene", age=11, guardian_id=bob.id, family_id=belcher_family.id)
)


@pytest.fixture
def pony(tina, belcher_family, create_model):
return create_model(
ToyModel(name="Pony", child_id=tina.id, family_id=belcher_family.id)
)


@pytest.fixture
def hat(louise, belcher_family, create_model):
return create_model(
ToyModel(name="Hat", child_id=louise.id, family_id=belcher_family.id)
)


@pytest.fixture
def keyboard(gene, belcher_family, create_model):
return create_model(
ToyModel(name="Keyboard", child_id=gene.id, family_id=belcher_family.id)
)


@pytest.fixture
def belchers(bob, tina, louise, gene, pony, hat, keyboard) -> None:
return
419 changes: 419 additions & 0 deletions tests/test.py

Large diffs are not rendered by default.

0 comments on commit 6172516

Please sign in to comment.