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/ 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/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/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 378024fa..2a81c97e 100644 --- a/fastapi_startkit/src/fastapi_startkit/exceptions/exceptions.py +++ b/fastapi_startkit/src/fastapi_startkit/exceptions/exceptions.py @@ -144,16 +144,6 @@ class NotificationException(Exception): pass -class ModelNotFoundException(Exception): - is_http_exception = True - - def get_response(self): - return "Model Not Found" - - def get_status(self): - return 404 - - class AuthorizationException(Exception): is_http_exception = True 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 d888e25b..9edfda5c 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.masoniteorm.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) 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..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,6 +4,7 @@ from ..fixtures.db import DB from ...fixtures.model import User from ..test_case import TestCase +from fastapi_startkit.masoniteorm import ModelNotFoundException class SqliteTestQueryBuilderModel(TestCase): @@ -43,6 +44,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"