fastapi-sdk 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,36 @@
1
+ Metadata-Version: 2.2
2
+ Name: fastapi-sdk
3
+ Version: 0.1.0
4
+ Summary: Utilities for FastAPI projects.
5
+ Requires-Python: >=3.13
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: fastapi[standard]>=0.115.11
8
+ Requires-Dist: odmantic>=1.0.2
9
+ Requires-Dist: pydantic>=2.10.6
10
+ Requires-Dist: pydantic-settings>=2.8.1
11
+ Requires-Dist: pytest>=8.3.5
12
+ Requires-Dist: pytest-asyncio>=0.25.3
13
+ Requires-Dist: shortuuid>=1.0.13
14
+
15
+ # FastAPI SDK
16
+
17
+ ## Run tests
18
+
19
+ ```bash
20
+ uv sync
21
+ pytest
22
+ ```
23
+
24
+ ## Update requirements
25
+
26
+ You can add new requirements by using UV:
27
+
28
+ ```bash
29
+ uv add module_name
30
+ ```
31
+
32
+ Then update the requirements.txt:
33
+
34
+ ```bash
35
+ uv pip compile pyproject.toml -o requirements.txt
36
+ ```
@@ -0,0 +1,22 @@
1
+ # FastAPI SDK
2
+
3
+ ## Run tests
4
+
5
+ ```bash
6
+ uv sync
7
+ pytest
8
+ ```
9
+
10
+ ## Update requirements
11
+
12
+ You can add new requirements by using UV:
13
+
14
+ ```bash
15
+ uv add module_name
16
+ ```
17
+
18
+ Then update the requirements.txt:
19
+
20
+ ```bash
21
+ uv pip compile pyproject.toml -o requirements.txt
22
+ ```
@@ -0,0 +1,109 @@
1
+ """Controller module for crud operations."""
2
+
3
+ from datetime import UTC, datetime
4
+ from typing import List, Optional, Type
5
+
6
+ from odmantic import AIOEngine, Model
7
+ from pydantic import BaseModel
8
+
9
+ from fastapi_sdk.utils.schema import datetime_now_sec
10
+
11
+
12
+ class Controller:
13
+ """Base controller class."""
14
+
15
+ model: Type[Model]
16
+ schema_create: Type[BaseModel]
17
+ schema_update: Type[BaseModel]
18
+ n_per_page: int = 10
19
+
20
+ def __init__(self, db_engine: AIOEngine):
21
+ """Initialize the controller."""
22
+ self.db_engine = db_engine
23
+
24
+ async def _create(self, **kwargs) -> BaseModel:
25
+ """Create a new model."""
26
+ data = self.schema_create(**kwargs)
27
+ model = self.model(**data.model_dump())
28
+ return await self.db_engine.save(model)
29
+
30
+ async def _update(self, uuid: str, data: dict) -> BaseModel:
31
+ """Update a model."""
32
+ model = await self._get(uuid)
33
+ data = self.schema_update(**data)
34
+ if model:
35
+ # Update the fields submitted
36
+ for field in data.model_dump(exclude_unset=True):
37
+ setattr(model, field, data.model_dump()[field])
38
+ model.updated_at = datetime_now_sec()
39
+ return await self.db_engine.save(model)
40
+ return None
41
+
42
+ async def _get(self, uuid: str) -> BaseModel:
43
+ """Get a model."""
44
+ return await self.db_engine.find_one(
45
+ self.model, self.model.uuid == uuid, self.model.deleted == False
46
+ )
47
+
48
+ async def _delete(self, uuid: str) -> BaseModel:
49
+ """Delete a model."""
50
+ model = await self._get(uuid)
51
+ if model:
52
+ model.deleted = True
53
+ return await self.db_engine.save(model)
54
+ return None
55
+
56
+ async def _list(
57
+ self,
58
+ page: int = 0,
59
+ query: Optional[List[dict]] = None,
60
+ order_by: Optional[dict] = None,
61
+ ) -> List[BaseModel]:
62
+ """List models."""
63
+ # Get the collection
64
+ collection_name = self.model.model_config[
65
+ "collection"
66
+ ] or self.model.__name__.lower().replace("model", "")
67
+ _collection = self.db_engine.database[collection_name]
68
+
69
+ # Create a pipeline for aggregation
70
+ _pipeline = []
71
+
72
+ # Filter out deleted models by default
73
+ # Example query: [{"due_date": {"$gte": start_date}}]
74
+ _query = {"deleted": False}
75
+ if query:
76
+ for q in query:
77
+ _query.update(q)
78
+
79
+ # Sorting, default by created_at
80
+ # Order by example: {"name": -1}, 1 ascending, -1 descending
81
+ _sort = order_by if order_by else {"created_at": -1}
82
+
83
+ # Add the pipeline stages
84
+ _pipeline.append({"$match": _query})
85
+ _pipeline.append({"$sort": _sort})
86
+
87
+ # Add pagination data
88
+ _pipeline.append({"$skip": (page - 1) * self.n_per_page if page > 0 else 0})
89
+ _pipeline.append({"$limit": self.n_per_page})
90
+
91
+ # Execute the aggregation
92
+ items = await _collection.aggregate(_pipeline).to_list(length=self.n_per_page)
93
+
94
+ # Count the total number of items
95
+ total = await _collection.count_documents(_query)
96
+
97
+ pages = total // self.n_per_page
98
+ if total % self.n_per_page > 0:
99
+ pages += 1
100
+
101
+ data = {
102
+ "items": [self.model.model_validate_doc(item) for item in items],
103
+ "total": total,
104
+ "size": len(items),
105
+ "page": page,
106
+ "pages": pages,
107
+ }
108
+
109
+ return data
@@ -0,0 +1,28 @@
1
+ """Set of utilities to create and manage models."""
2
+
3
+ import re
4
+ from typing import Annotated
5
+
6
+ import shortuuid
7
+ from pydantic import StringConstraints
8
+
9
+
10
+ class ShortUUID:
11
+ """Custom type for short UUIDs with trigram prefixes"""
12
+
13
+ @classmethod
14
+ def generate(cls, prefix: str) -> str:
15
+ """Generate a valid short UUID with a given trigram prefix"""
16
+ if not re.match(r"^[a-z]{3}$", prefix):
17
+ raise ValueError("Prefix must be exactly 3 lowercase letters")
18
+ return f"{prefix}_{shortuuid.uuid()[:10]}"
19
+
20
+ @classmethod
21
+ def validate(cls, value: str) -> str:
22
+ """Validate the short UUID format"""
23
+ if not re.match(r"^[a-z]{3}_[a-zA-Z0-9]{10}$", value):
24
+ raise ValueError("Invalid short UUID format")
25
+ return value
26
+
27
+
28
+ ShortUUIDType = Annotated[str, StringConstraints(pattern=r"^[a-z]{3}_[a-zA-Z0-9]{10}$")]
@@ -0,0 +1,8 @@
1
+ """Schema utilities."""
2
+
3
+ from datetime import UTC, datetime
4
+
5
+
6
+ def datetime_now_sec():
7
+ """Return the current datetime with microseconds set to 0."""
8
+ return datetime.now(UTC).replace(microsecond=0)
@@ -0,0 +1,36 @@
1
+ Metadata-Version: 2.2
2
+ Name: fastapi-sdk
3
+ Version: 0.1.0
4
+ Summary: Utilities for FastAPI projects.
5
+ Requires-Python: >=3.13
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: fastapi[standard]>=0.115.11
8
+ Requires-Dist: odmantic>=1.0.2
9
+ Requires-Dist: pydantic>=2.10.6
10
+ Requires-Dist: pydantic-settings>=2.8.1
11
+ Requires-Dist: pytest>=8.3.5
12
+ Requires-Dist: pytest-asyncio>=0.25.3
13
+ Requires-Dist: shortuuid>=1.0.13
14
+
15
+ # FastAPI SDK
16
+
17
+ ## Run tests
18
+
19
+ ```bash
20
+ uv sync
21
+ pytest
22
+ ```
23
+
24
+ ## Update requirements
25
+
26
+ You can add new requirements by using UV:
27
+
28
+ ```bash
29
+ uv add module_name
30
+ ```
31
+
32
+ Then update the requirements.txt:
33
+
34
+ ```bash
35
+ uv pip compile pyproject.toml -o requirements.txt
36
+ ```
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ fastapi_sdk/controller.py
4
+ fastapi_sdk.egg-info/PKG-INFO
5
+ fastapi_sdk.egg-info/SOURCES.txt
6
+ fastapi_sdk.egg-info/dependency_links.txt
7
+ fastapi_sdk.egg-info/requires.txt
8
+ fastapi_sdk.egg-info/top_level.txt
9
+ fastapi_sdk/utils/model.py
10
+ fastapi_sdk/utils/schema.py
11
+ tests/test_controller.py
@@ -0,0 +1,7 @@
1
+ fastapi[standard]>=0.115.11
2
+ odmantic>=1.0.2
3
+ pydantic>=2.10.6
4
+ pydantic-settings>=2.8.1
5
+ pytest>=8.3.5
6
+ pytest-asyncio>=0.25.3
7
+ shortuuid>=1.0.13
@@ -0,0 +1 @@
1
+ fastapi_sdk
@@ -0,0 +1,15 @@
1
+ [project]
2
+ name = "fastapi-sdk"
3
+ version = "0.1.0"
4
+ description = "Utilities for FastAPI projects."
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ dependencies = [
8
+ "fastapi[standard]>=0.115.11",
9
+ "odmantic>=1.0.2",
10
+ "pydantic>=2.10.6",
11
+ "pydantic-settings>=2.8.1",
12
+ "pytest>=8.3.5",
13
+ "pytest-asyncio>=0.25.3",
14
+ "shortuuid>=1.0.13",
15
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,67 @@
1
+ """Test controller."""
2
+
3
+ import time
4
+ from datetime import UTC
5
+
6
+ import pytest
7
+ from motor.core import AgnosticDatabase
8
+
9
+ from tests.controllers import Account
10
+
11
+
12
+ @pytest.mark.asyncio
13
+ async def test_controller(db_engine: AgnosticDatabase):
14
+ """Test controller."""
15
+
16
+ # Create two accounts, one for crud test and one for listing
17
+ account_1 = await Account(db_engine).create(name="Account 1")
18
+ account_2 = await Account(db_engine).create(name="Account 2")
19
+
20
+ assert account_1.uuid
21
+ assert account_1.name == "Account 1"
22
+ assert account_1.created_at
23
+ assert account_1.updated_at
24
+
25
+ # Get account
26
+ account_1 = await Account(db_engine).get(uuid=account_1.uuid)
27
+ assert account_1
28
+
29
+ # Sleep for 1 seconds to test updated_at
30
+ time.sleep(1)
31
+
32
+ # Update account
33
+ account_1 = await Account(db_engine).update(
34
+ uuid=account_1.uuid, data={"name": "Account 1 Updated"}
35
+ )
36
+
37
+ assert account_1.name == "Account 1 Updated"
38
+ assert account_1.updated_at > account_1.created_at.replace(tzinfo=UTC)
39
+
40
+ # List accounts
41
+ accounts = await Account(db_engine).list()
42
+ assert len(accounts["items"]) == 2
43
+ assert accounts["total"] == 2
44
+ assert accounts["page"] == 0
45
+ assert accounts["pages"] == 1
46
+
47
+ # Delete account
48
+ account_1 = await Account(db_engine).delete(uuid=account_1.uuid)
49
+ assert account_1.deleted is True
50
+
51
+ # Get deleted account
52
+ deleted_account = await Account(db_engine).get(uuid=account_1.uuid)
53
+ assert deleted_account is None
54
+
55
+ # Update deleted account
56
+ deleted_account = await Account(db_engine).update(
57
+ uuid=account_1.uuid, data={"name": "Account 1 Updated"}
58
+ )
59
+ assert deleted_account is None
60
+
61
+ # List accounts with one deleted
62
+ accounts = await Account(db_engine).list()
63
+ assert len(accounts["items"]) == 1
64
+ assert accounts["items"][0].uuid == account_2.uuid
65
+ assert accounts["total"] == 1
66
+ assert accounts["page"] == 0
67
+ assert accounts["pages"] == 1