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.
- fastapi_sdk-0.1.0/PKG-INFO +36 -0
- fastapi_sdk-0.1.0/README.md +22 -0
- fastapi_sdk-0.1.0/fastapi_sdk/controller.py +109 -0
- fastapi_sdk-0.1.0/fastapi_sdk/utils/model.py +28 -0
- fastapi_sdk-0.1.0/fastapi_sdk/utils/schema.py +8 -0
- fastapi_sdk-0.1.0/fastapi_sdk.egg-info/PKG-INFO +36 -0
- fastapi_sdk-0.1.0/fastapi_sdk.egg-info/SOURCES.txt +11 -0
- fastapi_sdk-0.1.0/fastapi_sdk.egg-info/dependency_links.txt +1 -0
- fastapi_sdk-0.1.0/fastapi_sdk.egg-info/requires.txt +7 -0
- fastapi_sdk-0.1.0/fastapi_sdk.egg-info/top_level.txt +1 -0
- fastapi_sdk-0.1.0/pyproject.toml +15 -0
- fastapi_sdk-0.1.0/setup.cfg +4 -0
- fastapi_sdk-0.1.0/tests/test_controller.py +67 -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,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,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 @@
|
|
|
1
|
+
|
|
@@ -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,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
|