From ae418acc036ce164d50c7ed20f366e755e612957 Mon Sep 17 00:00:00 2001 From: Bedram Tamang Date: Thu, 21 May 2026 15:17:14 -0700 Subject: [PATCH 1/7] feat: add find_or_fail method to Model and QueryBuilder Raises ModelNotFoundException (404) when no record matches the given primary key, mirroring the Laravel/Masonite find_or_fail convention. Co-Authored-By: Claude Sonnet 4.6 --- example/inertia-pingcrm-app/pyproject.toml | 1 + example/inertia-pingcrm-app/uv.lock | 89 ++++++++++++++++++- .../fastapi_startkit/exceptions/exceptions.py | 6 +- .../masoniteorm/models/builder.py | 10 +++ .../masoniteorm/models/model.py | 4 + 5 files changed, 108 insertions(+), 2 deletions(-) diff --git a/example/inertia-pingcrm-app/pyproject.toml b/example/inertia-pingcrm-app/pyproject.toml index 3fa4d2d7..aab4a084 100644 --- a/example/inertia-pingcrm-app/pyproject.toml +++ b/example/inertia-pingcrm-app/pyproject.toml @@ -20,6 +20,7 @@ dev = [ "dumpdie>=1.5.0", "pytest>=8.0", "pytest-asyncio>=0.24", + "pytest-playwright>=0.5", "httpx>=0.27", ] diff --git a/example/inertia-pingcrm-app/uv.lock b/example/inertia-pingcrm-app/uv.lock index 25319a63..eb226146 100644 --- a/example/inertia-pingcrm-app/uv.lock +++ b/example/inertia-pingcrm-app/uv.lock @@ -360,7 +360,7 @@ wheels = [ [[package]] name = "fastapi-startkit" -version = "0.20.0" +version = "0.26.0" source = { editable = "../../fastapi_startkit" } dependencies = [ { name = "cleo" }, @@ -409,11 +409,16 @@ provides-extras = ["fastapi", "database", "sqlite", "postgres", "mysql", "vite", [package.metadata.requires-dev] dev = [ + { name = "aiomysql", specifier = ">=0.2.0" }, + { name = "aiosqlite", specifier = ">=0.22.1" }, + { name = "asyncpg", specifier = ">=0.29.0" }, { name = "dumpdie", specifier = ">=1.5.0" }, + { name = "fastapi", extras = ["standard"], specifier = ">=0.124.4" }, { name = "itsdangerous", specifier = ">=2.2.0" }, { name = "pytest", specifier = ">=9.0.3" }, { name = "pytest-asyncio", specifier = ">=1.3.0" }, { name = "ruff", specifier = ">=0.9.0" }, + { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.38" }, { name = "twine", specifier = ">=6.2.0" }, ] @@ -630,6 +635,7 @@ dev = [ { name = "httpx" }, { name = "pytest" }, { name = "pytest-asyncio" }, + { name = "pytest-playwright" }, ] [package.metadata] @@ -648,6 +654,7 @@ dev = [ { name = "httpx", specifier = ">=0.27" }, { name = "pytest", specifier = ">=8.0" }, { name = "pytest-asyncio", specifier = ">=0.24" }, + { name = "pytest-playwright", specifier = ">=0.5" }, ] [[package]] @@ -834,6 +841,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/fb/d65db067a67df7252f18b0cb7420dda84078b9e8bfb375215469c14a50be/pendulum-3.2.0-py3-none-any.whl", hash = "sha256:f3a9c18a89b4d9ef39c5fa6a78722aaff8d5be2597c129a3b16b9f40a561acf3", size = 114111, upload-time = "2026-01-30T11:22:22.361Z" }, ] +[[package]] +name = "playwright" +version = "1.60.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet" }, + { name = "pyee" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/f0/832bd9677194908da118064eef20082f2791e3d18215cc6d9391ee2c5a67/playwright-1.60.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:6a8cd0fec171fb3089e95e898c8bc8a6f35dea0b78b399e12fcc19427e91b1d7", size = 43474635, upload-time = "2026-05-18T12:00:31.969Z" }, + { url = "https://files.pythonhosted.org/packages/59/7b/e1d32ae8a3ed937ec2be3721c5f728b13d731a0b7c6442e0b3bec5094ac0/playwright-1.60.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:39b5420ba6145045b69ced4c5c47d4d9fe5bddfc8ff816c518913afcb25ec7a5", size = 42261327, upload-time = "2026-05-18T12:00:35.638Z" }, + { url = "https://files.pythonhosted.org/packages/d7/bc/23de499ded6411c188a20c5a0dea6f0cd4ed5d2b3cc6042a5dbd3ed609aa/playwright-1.60.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:2581d0e6a3392c71f91b27460c7fd093356818dc430f48153896c8aeeaef7705", size = 43474636, upload-time = "2026-05-18T12:00:39.294Z" }, + { url = "https://files.pythonhosted.org/packages/22/7b/1d679f4fced4ea94efadd17103856d8c565384f68382a1681264e46f5925/playwright-1.60.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:1c2bfae7884fb3fb05b853290eab8f343d524e5016f2f1def702acbbdf14c93e", size = 47467220, upload-time = "2026-05-18T12:00:43.179Z" }, + { url = "https://files.pythonhosted.org/packages/84/c2/1528d267d4442bd2c6b8eaeab819dd52c2030bf80e89293f0ba1f687473b/playwright-1.60.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43e66564125ee31b07a58cefb21e256d62d67d8d1713e6858df7a3019d8ed353", size = 47154856, upload-time = "2026-05-18T12:00:46.715Z" }, + { url = "https://files.pythonhosted.org/packages/bb/4e/b008b6440a7a1624378041da94829956d4b8f7ab9ef5aad22d0dc3f2e26d/playwright-1.60.0-py3-none-win32.whl", hash = "sha256:ec94e416ea320711e0ad4bf185dcbf41833672961e90773e1885255d7db7b7e7", size = 37902157, upload-time = "2026-05-18T12:00:50.374Z" }, + { url = "https://files.pythonhosted.org/packages/55/f0/0541524133104f9cc20bf900870ff4a736b76a23483f3a55295ddfa58409/playwright-1.60.0-py3-none-win_amd64.whl", hash = "sha256:9566821ce6030a1f9e7146a24e19355ab0d98805fd0f9be50bb3d8fef1750c02", size = 37902159, upload-time = "2026-05-18T12:00:53.728Z" }, + { url = "https://files.pythonhosted.org/packages/80/c8/210f282d278e4709cdd71b12a31af45a30a22ab3207b387e29b37e478713/playwright-1.60.0-py3-none-win_arm64.whl", hash = "sha256:6e4f6700a4c2250efff8e690a81d66e3855754fb587b6b87cf5c784014f91537", size = 34037981, upload-time = "2026-05-18T12:00:57.584Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -938,6 +964,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/17/eb/9d89ad2d9b0ba8cd65393d434471621b98912abb10fbe1df08e480ba57b5/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", size = 2137657, upload-time = "2026-04-20T14:42:45.149Z" }, ] +[[package]] +name = "pyee" +version = "13.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/04/e7c1fe4dc78a6fdbfd6c337b1c3732ff543b8a397683ab38378447baa331/pyee-13.0.1.tar.gz", hash = "sha256:0b931f7c14535667ed4c7e0d531716368715e860b988770fc7eb8578d1f67fc8", size = 31655, upload-time = "2026-02-14T21:12:28.044Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/b4d4827c93ef43c01f599ef31453ccc1c132b353284fc6c87d535c233129/pyee-13.0.1-py3-none-any.whl", hash = "sha256:af2f8fede4171ef667dfded53f96e2ed0d6e6bd7ee3bb46437f77e3b57689228", size = 15659, upload-time = "2026-02-14T21:12:26.263Z" }, +] + [[package]] name = "pygments" version = "2.20.0" @@ -976,6 +1014,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, ] +[[package]] +name = "pytest-base-url" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/1a/b64ac368de6b993135cb70ca4e5d958a5c268094a3a2a4cac6f0021b6c4f/pytest_base_url-2.1.0.tar.gz", hash = "sha256:02748589a54f9e63fcbe62301d6b0496da0d10231b753e950c63e03aee745d45", size = 6702, upload-time = "2024-01-31T22:43:00.81Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/1c/b00940ab9eb8ede7897443b771987f2f4a76f06be02f1b3f01eb7567e24a/pytest_base_url-2.1.0-py3-none-any.whl", hash = "sha256:3ad15611778764d451927b2a53240c1a7a591b521ea44cebfe45849d2d2812e6", size = 5302, upload-time = "2024-01-31T22:42:58.897Z" }, +] + +[[package]] +name = "pytest-playwright" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "playwright" }, + { name = "pytest" }, + { name = "pytest-base-url" }, + { name = "python-slugify" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/ef/172eb8e23c80491fc72f1401c72f9305663873649351306a38b18406b0c9/pytest_playwright-0.8.0.tar.gz", hash = "sha256:7888d4a2443160c82e0c506c437076679f86b36d1910427250d90fbf1843a981", size = 17132, upload-time = "2026-05-18T10:16:15.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/71/1c545fac6a9054b52b3771238fb2dc6e8f1d0ccec116e1c7786ec191887c/pytest_playwright-0.8.0-py3-none-any.whl", hash = "sha256:856aae6efd4bc055f2ef229c647768760bcaad5cd3a5983c314ac260a974a933", size = 17143, upload-time = "2026-05-18T10:16:18.226Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -1006,6 +1072,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" }, ] +[[package]] +name = "python-slugify" +version = "8.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "text-unidecode" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/c7/5e1547c44e31da50a460df93af11a535ace568ef89d7a811069ead340c4a/python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856", size = 10921, upload-time = "2024-02-08T18:32:45.488Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051, upload-time = "2024-02-08T18:32:43.911Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -1332,6 +1410,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, ] +[[package]] +name = "text-unidecode" +version = "1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/e2/e9a00f0ccb71718418230718b3d900e71a5d16e701a3dae079a21e9cd8f8/text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93", size = 76885, upload-time = "2019-08-30T21:36:45.405Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154, upload-time = "2019-08-30T21:37:03.543Z" }, +] + [[package]] name = "typer" version = "0.24.1" diff --git a/fastapi_startkit/src/fastapi_startkit/exceptions/exceptions.py b/fastapi_startkit/src/fastapi_startkit/exceptions/exceptions.py index 378024fa..ca27a3df 100644 --- a/fastapi_startkit/src/fastapi_startkit/exceptions/exceptions.py +++ b/fastapi_startkit/src/fastapi_startkit/exceptions/exceptions.py @@ -147,8 +147,12 @@ class NotificationException(Exception): class ModelNotFoundException(Exception): is_http_exception = True + def __init__(self, message="Model Not Found"): + super().__init__(message) + self.message = message + def get_response(self): - return "Model Not Found" + return self.message def get_status(self): return 404 diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/builder.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/builder.py index d888e25b..4c980fe3 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/builder.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/builder.py @@ -87,6 +87,16 @@ def limit(self, limit: int) -> "QueryBuilder": async def find(self, primary_key: str | int, columns=None): return await self.where(self._model.__primary_key__, primary_key).first(columns) + async def find_or_fail(self, primary_key: str | int, columns=None): + from fastapi_startkit.exceptions.exceptions import ModelNotFoundException + + result = await self.find(primary_key, columns) + if result is None: + raise ModelNotFoundException( + f"{self._model.__class__.__name__} with primary key {primary_key!r} not found." + ) + return result + async def first(self, columns=None): if not columns: columns = [] diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/model.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/model.py index 6099aa8a..98f078f7 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/model.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/model.py @@ -103,6 +103,10 @@ def or_where_has(cls, relation: str, callback=None) -> "QueryBuilder": async def find(cls, primary_key: str | int, columns=None): return await cls.query().find(primary_key, columns) + @classmethod + async def find_or_fail(cls, primary_key: str | int, columns=None): + return await cls.query().find_or_fail(primary_key, columns) + @classmethod async def first(cls, columns=None): return await cls.query().first(columns) From 2f7c923eb9c0fee279622ffd71442db269b49fef Mon Sep 17 00:00:00 2001 From: Bedram Tamang Date: Thu, 21 May 2026 15:23:21 -0700 Subject: [PATCH 2/7] test: add tests for find_or_fail on Model and QueryBuilder Covers: returns model when found, raises ModelNotFoundException when not found, exception has 404 status, and exception message contains the key. Co-Authored-By: Claude Sonnet 4.6 --- .../sqlite/models/test_sqlite_model.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/fastapi_startkit/tests/masoniteorm/sqlite/models/test_sqlite_model.py b/fastapi_startkit/tests/masoniteorm/sqlite/models/test_sqlite_model.py index 9454471e..50d70a91 100644 --- a/fastapi_startkit/tests/masoniteorm/sqlite/models/test_sqlite_model.py +++ b/fastapi_startkit/tests/masoniteorm/sqlite/models/test_sqlite_model.py @@ -1,9 +1,12 @@ import unittest from unittest.mock import AsyncMock +import pytest + from ..fixtures.db import DB from ...fixtures.model import User from ..test_case import TestCase +from fastapi_startkit.exceptions.exceptions import ModelNotFoundException class SqliteTestQueryBuilderModel(TestCase): @@ -43,6 +46,27 @@ async def test_can_find_list(self): self.assertEqual(sql, 'SELECT * FROM "users" WHERE "users"."id" = ? LIMIT 1') self.assertIn(1, bindings) + async def test_find_or_fail_returns_model_when_found(self): + user = await User.find_or_fail(1) + self.assertIsInstance(user, User) + self.assertEqual(user.id, 1) + + async def test_find_or_fail_raises_when_not_found(self): + with self.assertRaises(ModelNotFoundException): + await User.find_or_fail(99999) + + async def test_find_or_fail_exception_has_404_status(self): + try: + await User.find_or_fail(99999) + except ModelNotFoundException as e: + self.assertEqual(e.get_status(), 404) + + async def test_find_or_fail_exception_message_contains_key(self): + try: + await User.find_or_fail(99999) + except ModelNotFoundException as e: + self.assertIn("99999", str(e)) + async def test_can_set_and_retrieve_attribute(self): user = await User.first() user.name = "updated" From 1f40e4a0582807e2f4ef5d7fc6d6c884347b4614 Mon Sep 17 00:00:00 2001 From: Bedram Tamang Date: Thu, 21 May 2026 15:25:56 -0700 Subject: [PATCH 3/7] feat: find_or_fail method added --- .claude/settings.json | 24 +++++ example/config-app/uv.lock | 5 ++ example/console-app/uv.lock | 14 ++- example/database-app/uv.lock | 5 ++ example/fastapi-app/uv.lock | 7 +- .../tests/browser/__init__.py | 0 .../tests/browser/test_auth.py | 89 +++++++++++++++++++ example/inertia-pingcrm-app/tests/conftest.py | 48 ++++++++++ example/vite-app/uv.lock | 23 ++++- .../sqlite/models/test_sqlite_model.py | 2 - 10 files changed, 210 insertions(+), 7 deletions(-) create mode 100644 .claude/settings.json create mode 100644 example/inertia-pingcrm-app/tests/browser/__init__.py create mode 100644 example/inertia-pingcrm-app/tests/browser/test_auth.py create mode 100644 example/inertia-pingcrm-app/tests/conftest.py diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..6897c0b4 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,24 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "http", + "url": "http://127.0.0.1:4545/api/claude-stopped" + } + ] + } + ], + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "http", + "url": "http://127.0.0.1:4545/api/claude-started" + } + ] + } + ] + } +} diff --git a/example/config-app/uv.lock b/example/config-app/uv.lock index 8a3ed7e4..f1a6cb64 100644 --- a/example/config-app/uv.lock +++ b/example/config-app/uv.lock @@ -198,11 +198,16 @@ provides-extras = ["fastapi", "database", "sqlite", "postgres", "mysql", "vite", [package.metadata.requires-dev] dev = [ + { name = "aiomysql", specifier = ">=0.2.0" }, + { name = "aiosqlite", specifier = ">=0.22.1" }, + { name = "asyncpg", specifier = ">=0.29.0" }, { name = "dumpdie", specifier = ">=1.5.0" }, + { name = "fastapi", extras = ["standard"], specifier = ">=0.124.4" }, { name = "itsdangerous", specifier = ">=2.2.0" }, { name = "pytest", specifier = ">=9.0.3" }, { name = "pytest-asyncio", specifier = ">=1.3.0" }, { name = "ruff", specifier = ">=0.9.0" }, + { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.38" }, { name = "twine", specifier = ">=6.2.0" }, ] diff --git a/example/console-app/uv.lock b/example/console-app/uv.lock index 35307a76..f37c7fc2 100644 --- a/example/console-app/uv.lock +++ b/example/console-app/uv.lock @@ -136,7 +136,7 @@ wheels = [ [[package]] name = "fastapi-startkit" -version = "0.13.6" +version = "0.26.0" source = { editable = "../../fastapi_startkit" } dependencies = [ { name = "cleo" }, @@ -159,19 +159,29 @@ requires-dist = [ { name = "faker", marker = "extra == 'database'", specifier = ">=40.13.0" }, { name = "fastapi", extras = ["standard"], marker = "extra == 'fastapi'", specifier = ">=0.124.4,<0.125.0" }, { name = "inflection", specifier = ">=0.5.1" }, + { name = "itsdangerous", marker = "extra == 'fastapi'", specifier = ">=2.2.0" }, + { name = "jinja2", marker = "extra == 'inertia'", specifier = ">=3.1" }, { name = "jinja2", marker = "extra == 'vite'", specifier = ">=3.1" }, + { name = "markupsafe", marker = "extra == 'inertia'", specifier = ">=2.0" }, { name = "pendulum", specifier = ">=3.1.0,<4.0.0" }, { name = "pydantic", specifier = ">=2.12.5" }, { name = "requests", specifier = ">=2.32.5,<3.0.0" }, { name = "sqlalchemy", extras = ["asyncio"], marker = "extra == 'database'", specifier = ">=2.0.38" }, ] -provides-extras = ["fastapi", "database", "sqlite", "postgres", "mysql", "vite"] +provides-extras = ["fastapi", "database", "sqlite", "postgres", "mysql", "vite", "inertia"] [package.metadata.requires-dev] dev = [ + { name = "aiomysql", specifier = ">=0.2.0" }, + { name = "aiosqlite", specifier = ">=0.22.1" }, + { name = "asyncpg", specifier = ">=0.29.0" }, { name = "dumpdie", specifier = ">=1.5.0" }, + { name = "fastapi", extras = ["standard"], specifier = ">=0.124.4" }, + { name = "itsdangerous", specifier = ">=2.2.0" }, { name = "pytest", specifier = ">=9.0.3" }, { name = "pytest-asyncio", specifier = ">=1.3.0" }, + { name = "ruff", specifier = ">=0.9.0" }, + { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.38" }, { name = "twine", specifier = ">=6.2.0" }, ] diff --git a/example/database-app/uv.lock b/example/database-app/uv.lock index 4fe12172..aa0ae8bb 100644 --- a/example/database-app/uv.lock +++ b/example/database-app/uv.lock @@ -547,11 +547,16 @@ provides-extras = ["fastapi", "database", "sqlite", "postgres", "mysql", "vite", [package.metadata.requires-dev] dev = [ + { name = "aiomysql", specifier = ">=0.2.0" }, + { name = "aiosqlite", specifier = ">=0.22.1" }, + { name = "asyncpg", specifier = ">=0.29.0" }, { name = "dumpdie", specifier = ">=1.5.0" }, + { name = "fastapi", extras = ["standard"], specifier = ">=0.124.4" }, { name = "itsdangerous", specifier = ">=2.2.0" }, { name = "pytest", specifier = ">=9.0.3" }, { name = "pytest-asyncio", specifier = ">=1.3.0" }, { name = "ruff", specifier = ">=0.9.0" }, + { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.38" }, { name = "twine", specifier = ">=6.2.0" }, ] diff --git a/example/fastapi-app/uv.lock b/example/fastapi-app/uv.lock index 4565ce5f..53eba160 100644 --- a/example/fastapi-app/uv.lock +++ b/example/fastapi-app/uv.lock @@ -299,7 +299,7 @@ wheels = [ [[package]] name = "fastapi-startkit" -version = "0.22.0" +version = "0.26.0" source = { editable = "../../fastapi_startkit" } dependencies = [ { name = "cleo" }, @@ -341,11 +341,16 @@ provides-extras = ["fastapi", "database", "sqlite", "postgres", "mysql", "vite", [package.metadata.requires-dev] dev = [ + { name = "aiomysql", specifier = ">=0.2.0" }, + { name = "aiosqlite", specifier = ">=0.22.1" }, + { name = "asyncpg", specifier = ">=0.29.0" }, { name = "dumpdie", specifier = ">=1.5.0" }, + { name = "fastapi", extras = ["standard"], specifier = ">=0.124.4" }, { name = "itsdangerous", specifier = ">=2.2.0" }, { name = "pytest", specifier = ">=9.0.3" }, { name = "pytest-asyncio", specifier = ">=1.3.0" }, { name = "ruff", specifier = ">=0.9.0" }, + { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.38" }, { name = "twine", specifier = ">=6.2.0" }, ] diff --git a/example/inertia-pingcrm-app/tests/browser/__init__.py b/example/inertia-pingcrm-app/tests/browser/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/example/inertia-pingcrm-app/tests/browser/test_auth.py b/example/inertia-pingcrm-app/tests/browser/test_auth.py new file mode 100644 index 00000000..a38fd6cb --- /dev/null +++ b/example/inertia-pingcrm-app/tests/browser/test_auth.py @@ -0,0 +1,89 @@ +import re +import pytest +from playwright.sync_api import Page, expect + +# Seeded credentials (databases/seeds/database_seeder.py) +EMAIL = "johndoe@example.com" +PASSWORD = "secret" + + +@pytest.fixture +def logged_in_page(page: Page) -> Page: + """Return a page already logged in as the seed user.""" + page.goto("/login") + page.locator("#email").fill(EMAIL) + page.locator('input[type="password"]').fill(PASSWORD) + page.get_by_role("button", name="Login").click() + page.wait_for_url(re.compile(r"/$")) + return page + + +# --------------------------------------------------------------------------- +# Login page +# --------------------------------------------------------------------------- + + +def test_login_page_renders(page: Page): + page.goto("/login") + expect(page).to_have_title(re.compile("Login")) + expect(page.locator("#email")).to_be_visible() + expect(page.locator('input[type="password"]')).to_be_visible() + expect(page.get_by_role("button", name="Login")).to_be_visible() + + +def test_login_prefills_demo_credentials(page: Page): + """The seed user's credentials are pre-filled in the login form.""" + page.goto("/login") + expect(page.locator("#email")).to_have_value(EMAIL) + expect(page.locator('input[type="password"]')).to_have_value(PASSWORD) + + +# --------------------------------------------------------------------------- +# Authentication flows +# --------------------------------------------------------------------------- + + +def test_valid_login_redirects_to_dashboard(page: Page): + page.goto("/login") + page.locator("#email").fill(EMAIL) + page.locator('input[type="password"]').fill(PASSWORD) + page.get_by_role("button", name="Login").click() + expect(page).to_have_url(re.compile(r"/$")) + expect(page.get_by_role("heading", name="Dashboard")).to_be_visible() + + +def test_invalid_login_shows_error(page: Page): + page.goto("/login") + page.locator("#email").fill("nobody@example.com") + page.locator('input[type="password"]').fill("wrong") + page.get_by_role("button", name="Login").click() + expect(page).to_have_url(re.compile(r"/login")) + expect( + page.get_by_text("These credentials do not match our records.") + ).to_be_visible() + + +def test_protected_route_redirects_unauthenticated_user(page: Page): + page.goto("/") + expect(page).to_have_url(re.compile(r"/login")) + + +# --------------------------------------------------------------------------- +# Logout +# --------------------------------------------------------------------------- + + +def test_logout_returns_to_login(logged_in_page: Page): + page = logged_in_page + # Open the user dropdown (shows "John" / "John Doe" depending on viewport) + page.get_by_text("John").first.click() + page.get_by_role("button", name="Logout").click() + expect(page).to_have_url(re.compile(r"/login")) + + +def test_after_logout_protected_routes_require_login(logged_in_page: Page): + page = logged_in_page + page.get_by_text("John").first.click() + page.get_by_role("button", name="Logout").click() + page.goto("/") + expect(page).to_have_url(re.compile(r"/login")) diff --git a/example/inertia-pingcrm-app/tests/conftest.py b/example/inertia-pingcrm-app/tests/conftest.py new file mode 100644 index 00000000..d58e6c8a --- /dev/null +++ b/example/inertia-pingcrm-app/tests/conftest.py @@ -0,0 +1,48 @@ +import socket +import time +import threading +import pytest + + +def _find_free_port() -> int: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("127.0.0.1", 0)) + return s.getsockname()[1] + + +@pytest.fixture(scope="session") +def live_server_url(): + """Start the app on a free port and yield its base URL.""" + import uvicorn + import httpx + from bootstrap.application import app + + host = "127.0.0.1" + port = _find_free_port() + + config = uvicorn.Config(app, host=host, port=port, log_level="error") + server = uvicorn.Server(config) + thread = threading.Thread(target=server.run, daemon=True) + thread.start() + + base_url = f"http://{host}:{port}" + deadline = time.time() + 15 + while time.time() < deadline: + try: + httpx.get(f"{base_url}/login", follow_redirects=False, timeout=1.0) + break + except Exception: + time.sleep(0.3) + else: + raise RuntimeError("Test server did not start within 15 seconds") + + yield base_url + + server.should_exit = True + thread.join(timeout=5) + + +@pytest.fixture(scope="session") +def base_url(live_server_url): + """Override pytest-playwright's base_url with the dynamic live server URL.""" + return live_server_url diff --git a/example/vite-app/uv.lock b/example/vite-app/uv.lock index 698dbf91..47c56698 100644 --- a/example/vite-app/uv.lock +++ b/example/vite-app/uv.lock @@ -280,7 +280,7 @@ wheels = [ [[package]] name = "fastapi-startkit" -version = "0.13.6" +version = "0.26.0" source = { editable = "../../fastapi_startkit" } dependencies = [ { name = "cleo" }, @@ -295,6 +295,7 @@ dependencies = [ [package.optional-dependencies] fastapi = [ { name = "fastapi", extra = ["standard"] }, + { name = "itsdangerous" }, ] vite = [ { name = "jinja2" }, @@ -311,20 +312,29 @@ requires-dist = [ { name = "faker", marker = "extra == 'database'", specifier = ">=40.13.0" }, { name = "fastapi", extras = ["standard"], marker = "extra == 'fastapi'", specifier = ">=0.124.4,<0.125.0" }, { name = "inflection", specifier = ">=0.5.1" }, + { name = "itsdangerous", marker = "extra == 'fastapi'", specifier = ">=2.2.0" }, + { name = "jinja2", marker = "extra == 'inertia'", specifier = ">=3.1" }, { name = "jinja2", marker = "extra == 'vite'", specifier = ">=3.1" }, + { name = "markupsafe", marker = "extra == 'inertia'", specifier = ">=2.0" }, { name = "pendulum", specifier = ">=3.1.0,<4.0.0" }, { name = "pydantic", specifier = ">=2.12.5" }, { name = "requests", specifier = ">=2.32.5,<3.0.0" }, { name = "sqlalchemy", extras = ["asyncio"], marker = "extra == 'database'", specifier = ">=2.0.38" }, ] -provides-extras = ["fastapi", "database", "sqlite", "postgres", "mysql", "vite"] +provides-extras = ["fastapi", "database", "sqlite", "postgres", "mysql", "vite", "inertia"] [package.metadata.requires-dev] dev = [ + { name = "aiomysql", specifier = ">=0.2.0" }, + { name = "aiosqlite", specifier = ">=0.22.1" }, + { name = "asyncpg", specifier = ">=0.29.0" }, { name = "dumpdie", specifier = ">=1.5.0" }, + { name = "fastapi", extras = ["standard"], specifier = ">=0.124.4" }, + { name = "itsdangerous", specifier = ">=2.2.0" }, { name = "pytest", specifier = ">=9.0.3" }, { name = "pytest-asyncio", specifier = ">=1.3.0" }, { name = "ruff", specifier = ">=0.9.0" }, + { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.38" }, { name = "twine", specifier = ">=6.2.0" }, ] @@ -484,6 +494,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/59/91/aa6bde563e0085a02a435aa99b49ef75b0a4b062635e606dab23ce18d720/inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2", size = 9454, upload-time = "2020-08-22T08:16:27.816Z" }, ] +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" diff --git a/fastapi_startkit/tests/masoniteorm/sqlite/models/test_sqlite_model.py b/fastapi_startkit/tests/masoniteorm/sqlite/models/test_sqlite_model.py index 50d70a91..eeef9e4c 100644 --- a/fastapi_startkit/tests/masoniteorm/sqlite/models/test_sqlite_model.py +++ b/fastapi_startkit/tests/masoniteorm/sqlite/models/test_sqlite_model.py @@ -1,8 +1,6 @@ import unittest from unittest.mock import AsyncMock -import pytest - from ..fixtures.db import DB from ...fixtures.model import User from ..test_case import TestCase From 5c757d159207d0e0310364e8dbecd6c456dc560e Mon Sep 17 00:00:00 2001 From: Bedram Tamang Date: Thu, 21 May 2026 15:41:41 -0700 Subject: [PATCH 4/7] chore: remove pingcrm changes from find-or-fail branch These belong in their own dedicated PR. Co-Authored-By: Claude Sonnet 4.6 --- example/inertia-pingcrm-app/pyproject.toml | 1 - .../tests/browser/__init__.py | 0 .../tests/browser/test_auth.py | 89 ------------------- example/inertia-pingcrm-app/tests/conftest.py | 48 ---------- example/inertia-pingcrm-app/uv.lock | 89 +------------------ 5 files changed, 1 insertion(+), 226 deletions(-) delete mode 100644 example/inertia-pingcrm-app/tests/browser/__init__.py delete mode 100644 example/inertia-pingcrm-app/tests/browser/test_auth.py delete mode 100644 example/inertia-pingcrm-app/tests/conftest.py diff --git a/example/inertia-pingcrm-app/pyproject.toml b/example/inertia-pingcrm-app/pyproject.toml index aab4a084..3fa4d2d7 100644 --- a/example/inertia-pingcrm-app/pyproject.toml +++ b/example/inertia-pingcrm-app/pyproject.toml @@ -20,7 +20,6 @@ dev = [ "dumpdie>=1.5.0", "pytest>=8.0", "pytest-asyncio>=0.24", - "pytest-playwright>=0.5", "httpx>=0.27", ] diff --git a/example/inertia-pingcrm-app/tests/browser/__init__.py b/example/inertia-pingcrm-app/tests/browser/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/example/inertia-pingcrm-app/tests/browser/test_auth.py b/example/inertia-pingcrm-app/tests/browser/test_auth.py deleted file mode 100644 index a38fd6cb..00000000 --- a/example/inertia-pingcrm-app/tests/browser/test_auth.py +++ /dev/null @@ -1,89 +0,0 @@ -import re -import pytest -from playwright.sync_api import Page, expect - -# Seeded credentials (databases/seeds/database_seeder.py) -EMAIL = "johndoe@example.com" -PASSWORD = "secret" - - -@pytest.fixture -def logged_in_page(page: Page) -> Page: - """Return a page already logged in as the seed user.""" - page.goto("/login") - page.locator("#email").fill(EMAIL) - page.locator('input[type="password"]').fill(PASSWORD) - page.get_by_role("button", name="Login").click() - page.wait_for_url(re.compile(r"/$")) - return page - - -# --------------------------------------------------------------------------- -# Login page -# --------------------------------------------------------------------------- - - -def test_login_page_renders(page: Page): - page.goto("/login") - expect(page).to_have_title(re.compile("Login")) - expect(page.locator("#email")).to_be_visible() - expect(page.locator('input[type="password"]')).to_be_visible() - expect(page.get_by_role("button", name="Login")).to_be_visible() - - -def test_login_prefills_demo_credentials(page: Page): - """The seed user's credentials are pre-filled in the login form.""" - page.goto("/login") - expect(page.locator("#email")).to_have_value(EMAIL) - expect(page.locator('input[type="password"]')).to_have_value(PASSWORD) - - -# --------------------------------------------------------------------------- -# Authentication flows -# --------------------------------------------------------------------------- - - -def test_valid_login_redirects_to_dashboard(page: Page): - page.goto("/login") - page.locator("#email").fill(EMAIL) - page.locator('input[type="password"]').fill(PASSWORD) - page.get_by_role("button", name="Login").click() - expect(page).to_have_url(re.compile(r"/$")) - expect(page.get_by_role("heading", name="Dashboard")).to_be_visible() - - -def test_invalid_login_shows_error(page: Page): - page.goto("/login") - page.locator("#email").fill("nobody@example.com") - page.locator('input[type="password"]').fill("wrong") - page.get_by_role("button", name="Login").click() - expect(page).to_have_url(re.compile(r"/login")) - expect( - page.get_by_text("These credentials do not match our records.") - ).to_be_visible() - - -def test_protected_route_redirects_unauthenticated_user(page: Page): - page.goto("/") - expect(page).to_have_url(re.compile(r"/login")) - - -# --------------------------------------------------------------------------- -# Logout -# --------------------------------------------------------------------------- - - -def test_logout_returns_to_login(logged_in_page: Page): - page = logged_in_page - # Open the user dropdown (shows "John" / "John Doe" depending on viewport) - page.get_by_text("John").first.click() - page.get_by_role("button", name="Logout").click() - expect(page).to_have_url(re.compile(r"/login")) - - -def test_after_logout_protected_routes_require_login(logged_in_page: Page): - page = logged_in_page - page.get_by_text("John").first.click() - page.get_by_role("button", name="Logout").click() - page.goto("/") - expect(page).to_have_url(re.compile(r"/login")) diff --git a/example/inertia-pingcrm-app/tests/conftest.py b/example/inertia-pingcrm-app/tests/conftest.py deleted file mode 100644 index d58e6c8a..00000000 --- a/example/inertia-pingcrm-app/tests/conftest.py +++ /dev/null @@ -1,48 +0,0 @@ -import socket -import time -import threading -import pytest - - -def _find_free_port() -> int: - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.bind(("127.0.0.1", 0)) - return s.getsockname()[1] - - -@pytest.fixture(scope="session") -def live_server_url(): - """Start the app on a free port and yield its base URL.""" - import uvicorn - import httpx - from bootstrap.application import app - - host = "127.0.0.1" - port = _find_free_port() - - config = uvicorn.Config(app, host=host, port=port, log_level="error") - server = uvicorn.Server(config) - thread = threading.Thread(target=server.run, daemon=True) - thread.start() - - base_url = f"http://{host}:{port}" - deadline = time.time() + 15 - while time.time() < deadline: - try: - httpx.get(f"{base_url}/login", follow_redirects=False, timeout=1.0) - break - except Exception: - time.sleep(0.3) - else: - raise RuntimeError("Test server did not start within 15 seconds") - - yield base_url - - server.should_exit = True - thread.join(timeout=5) - - -@pytest.fixture(scope="session") -def base_url(live_server_url): - """Override pytest-playwright's base_url with the dynamic live server URL.""" - return live_server_url diff --git a/example/inertia-pingcrm-app/uv.lock b/example/inertia-pingcrm-app/uv.lock index eb226146..25319a63 100644 --- a/example/inertia-pingcrm-app/uv.lock +++ b/example/inertia-pingcrm-app/uv.lock @@ -360,7 +360,7 @@ wheels = [ [[package]] name = "fastapi-startkit" -version = "0.26.0" +version = "0.20.0" source = { editable = "../../fastapi_startkit" } dependencies = [ { name = "cleo" }, @@ -409,16 +409,11 @@ provides-extras = ["fastapi", "database", "sqlite", "postgres", "mysql", "vite", [package.metadata.requires-dev] dev = [ - { name = "aiomysql", specifier = ">=0.2.0" }, - { name = "aiosqlite", specifier = ">=0.22.1" }, - { name = "asyncpg", specifier = ">=0.29.0" }, { name = "dumpdie", specifier = ">=1.5.0" }, - { name = "fastapi", extras = ["standard"], specifier = ">=0.124.4" }, { name = "itsdangerous", specifier = ">=2.2.0" }, { name = "pytest", specifier = ">=9.0.3" }, { name = "pytest-asyncio", specifier = ">=1.3.0" }, { name = "ruff", specifier = ">=0.9.0" }, - { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.38" }, { name = "twine", specifier = ">=6.2.0" }, ] @@ -635,7 +630,6 @@ dev = [ { name = "httpx" }, { name = "pytest" }, { name = "pytest-asyncio" }, - { name = "pytest-playwright" }, ] [package.metadata] @@ -654,7 +648,6 @@ dev = [ { name = "httpx", specifier = ">=0.27" }, { name = "pytest", specifier = ">=8.0" }, { name = "pytest-asyncio", specifier = ">=0.24" }, - { name = "pytest-playwright", specifier = ">=0.5" }, ] [[package]] @@ -841,25 +834,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/fb/d65db067a67df7252f18b0cb7420dda84078b9e8bfb375215469c14a50be/pendulum-3.2.0-py3-none-any.whl", hash = "sha256:f3a9c18a89b4d9ef39c5fa6a78722aaff8d5be2597c129a3b16b9f40a561acf3", size = 114111, upload-time = "2026-01-30T11:22:22.361Z" }, ] -[[package]] -name = "playwright" -version = "1.60.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "greenlet" }, - { name = "pyee" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/21/f0/832bd9677194908da118064eef20082f2791e3d18215cc6d9391ee2c5a67/playwright-1.60.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:6a8cd0fec171fb3089e95e898c8bc8a6f35dea0b78b399e12fcc19427e91b1d7", size = 43474635, upload-time = "2026-05-18T12:00:31.969Z" }, - { url = "https://files.pythonhosted.org/packages/59/7b/e1d32ae8a3ed937ec2be3721c5f728b13d731a0b7c6442e0b3bec5094ac0/playwright-1.60.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:39b5420ba6145045b69ced4c5c47d4d9fe5bddfc8ff816c518913afcb25ec7a5", size = 42261327, upload-time = "2026-05-18T12:00:35.638Z" }, - { url = "https://files.pythonhosted.org/packages/d7/bc/23de499ded6411c188a20c5a0dea6f0cd4ed5d2b3cc6042a5dbd3ed609aa/playwright-1.60.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:2581d0e6a3392c71f91b27460c7fd093356818dc430f48153896c8aeeaef7705", size = 43474636, upload-time = "2026-05-18T12:00:39.294Z" }, - { url = "https://files.pythonhosted.org/packages/22/7b/1d679f4fced4ea94efadd17103856d8c565384f68382a1681264e46f5925/playwright-1.60.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:1c2bfae7884fb3fb05b853290eab8f343d524e5016f2f1def702acbbdf14c93e", size = 47467220, upload-time = "2026-05-18T12:00:43.179Z" }, - { url = "https://files.pythonhosted.org/packages/84/c2/1528d267d4442bd2c6b8eaeab819dd52c2030bf80e89293f0ba1f687473b/playwright-1.60.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43e66564125ee31b07a58cefb21e256d62d67d8d1713e6858df7a3019d8ed353", size = 47154856, upload-time = "2026-05-18T12:00:46.715Z" }, - { url = "https://files.pythonhosted.org/packages/bb/4e/b008b6440a7a1624378041da94829956d4b8f7ab9ef5aad22d0dc3f2e26d/playwright-1.60.0-py3-none-win32.whl", hash = "sha256:ec94e416ea320711e0ad4bf185dcbf41833672961e90773e1885255d7db7b7e7", size = 37902157, upload-time = "2026-05-18T12:00:50.374Z" }, - { url = "https://files.pythonhosted.org/packages/55/f0/0541524133104f9cc20bf900870ff4a736b76a23483f3a55295ddfa58409/playwright-1.60.0-py3-none-win_amd64.whl", hash = "sha256:9566821ce6030a1f9e7146a24e19355ab0d98805fd0f9be50bb3d8fef1750c02", size = 37902159, upload-time = "2026-05-18T12:00:53.728Z" }, - { url = "https://files.pythonhosted.org/packages/80/c8/210f282d278e4709cdd71b12a31af45a30a22ab3207b387e29b37e478713/playwright-1.60.0-py3-none-win_arm64.whl", hash = "sha256:6e4f6700a4c2250efff8e690a81d66e3855754fb587b6b87cf5c784014f91537", size = 34037981, upload-time = "2026-05-18T12:00:57.584Z" }, -] - [[package]] name = "pluggy" version = "1.6.0" @@ -964,18 +938,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/17/eb/9d89ad2d9b0ba8cd65393d434471621b98912abb10fbe1df08e480ba57b5/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", size = 2137657, upload-time = "2026-04-20T14:42:45.149Z" }, ] -[[package]] -name = "pyee" -version = "13.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8b/04/e7c1fe4dc78a6fdbfd6c337b1c3732ff543b8a397683ab38378447baa331/pyee-13.0.1.tar.gz", hash = "sha256:0b931f7c14535667ed4c7e0d531716368715e860b988770fc7eb8578d1f67fc8", size = 31655, upload-time = "2026-02-14T21:12:28.044Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/c4/b4d4827c93ef43c01f599ef31453ccc1c132b353284fc6c87d535c233129/pyee-13.0.1-py3-none-any.whl", hash = "sha256:af2f8fede4171ef667dfded53f96e2ed0d6e6bd7ee3bb46437f77e3b57689228", size = 15659, upload-time = "2026-02-14T21:12:26.263Z" }, -] - [[package]] name = "pygments" version = "2.20.0" @@ -1014,34 +976,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, ] -[[package]] -name = "pytest-base-url" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ae/1a/b64ac368de6b993135cb70ca4e5d958a5c268094a3a2a4cac6f0021b6c4f/pytest_base_url-2.1.0.tar.gz", hash = "sha256:02748589a54f9e63fcbe62301d6b0496da0d10231b753e950c63e03aee745d45", size = 6702, upload-time = "2024-01-31T22:43:00.81Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/1c/b00940ab9eb8ede7897443b771987f2f4a76f06be02f1b3f01eb7567e24a/pytest_base_url-2.1.0-py3-none-any.whl", hash = "sha256:3ad15611778764d451927b2a53240c1a7a591b521ea44cebfe45849d2d2812e6", size = 5302, upload-time = "2024-01-31T22:42:58.897Z" }, -] - -[[package]] -name = "pytest-playwright" -version = "0.8.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "playwright" }, - { name = "pytest" }, - { name = "pytest-base-url" }, - { name = "python-slugify" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/58/ef/172eb8e23c80491fc72f1401c72f9305663873649351306a38b18406b0c9/pytest_playwright-0.8.0.tar.gz", hash = "sha256:7888d4a2443160c82e0c506c437076679f86b36d1910427250d90fbf1843a981", size = 17132, upload-time = "2026-05-18T10:16:15.919Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/71/1c545fac6a9054b52b3771238fb2dc6e8f1d0ccec116e1c7786ec191887c/pytest_playwright-0.8.0-py3-none-any.whl", hash = "sha256:856aae6efd4bc055f2ef229c647768760bcaad5cd3a5983c314ac260a974a933", size = 17143, upload-time = "2026-05-18T10:16:18.226Z" }, -] - [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -1072,18 +1006,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" }, ] -[[package]] -name = "python-slugify" -version = "8.0.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "text-unidecode" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/87/c7/5e1547c44e31da50a460df93af11a535ace568ef89d7a811069ead340c4a/python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856", size = 10921, upload-time = "2024-02-08T18:32:45.488Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051, upload-time = "2024-02-08T18:32:43.911Z" }, -] - [[package]] name = "pyyaml" version = "6.0.3" @@ -1410,15 +1332,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, ] -[[package]] -name = "text-unidecode" -version = "1.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ab/e2/e9a00f0ccb71718418230718b3d900e71a5d16e701a3dae079a21e9cd8f8/text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93", size = 76885, upload-time = "2019-08-30T21:36:45.405Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154, upload-time = "2019-08-30T21:37:03.543Z" }, -] - [[package]] name = "typer" version = "0.24.1" From ae52422dd087d35c4c8a380e81c3fa77202f0253 Mon Sep 17 00:00:00 2001 From: Bedram Tamang Date: Thu, 21 May 2026 15:44:40 -0700 Subject: [PATCH 5/7] refactor: move ModelNotFoundException into masoniteorm - Add fastapi_startkit/masoniteorm/exceptions.py as the canonical home - Export ModelNotFoundException from fastapi_startkit.masoniteorm - Keep a re-export in fastapi_startkit.exceptions for backwards compatibility Co-Authored-By: Claude Sonnet 4.6 --- .../src/fastapi_startkit/exceptions/exceptions.py | 13 +------------ .../src/fastapi_startkit/masoniteorm/__init__.py | 2 ++ .../src/fastapi_startkit/masoniteorm/exceptions.py | 12 ++++++++++++ .../fastapi_startkit/masoniteorm/models/builder.py | 2 +- .../masoniteorm/sqlite/models/test_sqlite_model.py | 2 +- 5 files changed, 17 insertions(+), 14 deletions(-) create mode 100644 fastapi_startkit/src/fastapi_startkit/masoniteorm/exceptions.py diff --git a/fastapi_startkit/src/fastapi_startkit/exceptions/exceptions.py b/fastapi_startkit/src/fastapi_startkit/exceptions/exceptions.py index ca27a3df..d3b3b21f 100644 --- a/fastapi_startkit/src/fastapi_startkit/exceptions/exceptions.py +++ b/fastapi_startkit/src/fastapi_startkit/exceptions/exceptions.py @@ -144,18 +144,7 @@ class NotificationException(Exception): pass -class ModelNotFoundException(Exception): - is_http_exception = True - - def __init__(self, message="Model Not Found"): - super().__init__(message) - self.message = message - - def get_response(self): - return self.message - - def get_status(self): - return 404 +from fastapi_startkit.masoniteorm.exceptions import ModelNotFoundException # noqa: F401 class AuthorizationException(Exception): diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/__init__.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/__init__.py index b9117281..cab8e8b2 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/__init__.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/__init__.py @@ -1,4 +1,5 @@ from .config.config import MySQLConfig, PostgresConfig, SQLiteConfig +from .exceptions import ModelNotFoundException from .facades import DB from .migrations.Migration import Migration from .migrations.Migrator import Migrator @@ -13,6 +14,7 @@ "MySQLConfig", "SQLiteConfig", "Model", + "ModelNotFoundException", "DB", "Migration", "Migrator", diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/exceptions.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/exceptions.py new file mode 100644 index 00000000..3ac0b806 --- /dev/null +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/exceptions.py @@ -0,0 +1,12 @@ +class ModelNotFoundException(Exception): + is_http_exception = True + + def __init__(self, message="Model Not Found"): + super().__init__(message) + self.message = message + + def get_response(self): + return self.message + + def get_status(self): + return 404 diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/builder.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/builder.py index 4c980fe3..9edfda5c 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/builder.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/builder.py @@ -88,7 +88,7 @@ async def find(self, primary_key: str | int, columns=None): return await self.where(self._model.__primary_key__, primary_key).first(columns) async def find_or_fail(self, primary_key: str | int, columns=None): - from fastapi_startkit.exceptions.exceptions import ModelNotFoundException + from fastapi_startkit.masoniteorm.exceptions import ModelNotFoundException result = await self.find(primary_key, columns) if result is None: diff --git a/fastapi_startkit/tests/masoniteorm/sqlite/models/test_sqlite_model.py b/fastapi_startkit/tests/masoniteorm/sqlite/models/test_sqlite_model.py index eeef9e4c..facd4be0 100644 --- a/fastapi_startkit/tests/masoniteorm/sqlite/models/test_sqlite_model.py +++ b/fastapi_startkit/tests/masoniteorm/sqlite/models/test_sqlite_model.py @@ -4,7 +4,7 @@ from ..fixtures.db import DB from ...fixtures.model import User from ..test_case import TestCase -from fastapi_startkit.exceptions.exceptions import ModelNotFoundException +from fastapi_startkit.masoniteorm import ModelNotFoundException class SqliteTestQueryBuilderModel(TestCase): From 9e85fec81f0f1df4d7f45fa05ba950e22c6f98ba Mon Sep 17 00:00:00 2001 From: Bedram Tamang Date: Thu, 21 May 2026 15:48:10 -0700 Subject: [PATCH 6/7] chore: remove .claude/settings.json from git and ignore .claude/ --- .claude/settings.json | 24 ------------------------ .gitignore | 1 + 2 files changed, 1 insertion(+), 24 deletions(-) delete mode 100644 .claude/settings.json diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index 6897c0b4..00000000 --- a/.claude/settings.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "hooks": { - "Stop": [ - { - "hooks": [ - { - "type": "http", - "url": "http://127.0.0.1:4545/api/claude-stopped" - } - ] - } - ], - "UserPromptSubmit": [ - { - "hooks": [ - { - "type": "http", - "url": "http://127.0.0.1:4545/api/claude-started" - } - ] - } - ] - } -} diff --git a/.gitignore b/.gitignore index e0930bcd..ee65e917 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ laravel-repo application .agents .uv_cache +.claude/ From b15a1ae77889324b825f9fdbb19e1ca4e572680c Mon Sep 17 00:00:00 2001 From: Bedram Tamang Date: Thu, 21 May 2026 15:50:42 -0700 Subject: [PATCH 7/7] chore: remove ModelNotFoundException re-export from fastapi exceptions --- fastapi_startkit/src/fastapi_startkit/exceptions/__init__.py | 1 - fastapi_startkit/src/fastapi_startkit/exceptions/exceptions.py | 3 --- 2 files changed, 4 deletions(-) diff --git a/fastapi_startkit/src/fastapi_startkit/exceptions/__init__.py b/fastapi_startkit/src/fastapi_startkit/exceptions/__init__.py index a8dc7b24..fae4175e 100644 --- a/fastapi_startkit/src/fastapi_startkit/exceptions/__init__.py +++ b/fastapi_startkit/src/fastapi_startkit/exceptions/__init__.py @@ -30,6 +30,5 @@ ValidationException, AmbiguousError, MethodNotAllowedException, - ModelNotFoundException, ThrottleRequestsException, ) diff --git a/fastapi_startkit/src/fastapi_startkit/exceptions/exceptions.py b/fastapi_startkit/src/fastapi_startkit/exceptions/exceptions.py index d3b3b21f..2a81c97e 100644 --- a/fastapi_startkit/src/fastapi_startkit/exceptions/exceptions.py +++ b/fastapi_startkit/src/fastapi_startkit/exceptions/exceptions.py @@ -144,9 +144,6 @@ class NotificationException(Exception): pass -from fastapi_startkit.masoniteorm.exceptions import ModelNotFoundException # noqa: F401 - - class AuthorizationException(Exception): is_http_exception = True