fastapi-mongo-base 0.12.2__tar.gz → 0.12.4__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.
Files changed (29) hide show
  1. fastapi_mongo_base-0.12.4/MANIFEST.in +1 -0
  2. {fastapi_mongo_base-0.12.2/src/fastapi_mongo_base.egg-info → fastapi_mongo_base-0.12.4}/PKG-INFO +4 -22
  3. {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/pyproject.toml +30 -12
  4. {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/core/app_factory.py +37 -16
  5. {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/core/config.py +1 -1
  6. {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/core/db.py +2 -2
  7. {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/core/enums.py +2 -2
  8. {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/core/exceptions.py +5 -2
  9. {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/models.py +86 -41
  10. {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/routes.py +32 -13
  11. {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/schemas.py +5 -16
  12. {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/sql.py +24 -16
  13. {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/tasks.py +43 -18
  14. {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/utils/basic.py +12 -5
  15. {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/utils/bsontools.py +1 -1
  16. {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4/src/fastapi_mongo_base.egg-info}/PKG-INFO +4 -22
  17. {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base.egg-info/SOURCES.txt +1 -0
  18. {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base.egg-info/requires.txt +2 -0
  19. {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/LICENSE.txt +0 -0
  20. {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/README.md +0 -0
  21. {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/setup.cfg +0 -0
  22. {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/__init__.py +0 -0
  23. {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/core/__init__.py +0 -0
  24. {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/core/middlewares.py +0 -0
  25. {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/core/timezone.py +0 -0
  26. {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/utils/__init__.py +0 -0
  27. {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base/utils/conditions.py +0 -0
  28. {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base.egg-info/dependency_links.txt +0 -0
  29. {fastapi_mongo_base-0.12.2 → fastapi_mongo_base-0.12.4}/src/fastapi_mongo_base.egg-info/top_level.txt +0 -0
@@ -0,0 +1 @@
1
+ include pytest.ini
@@ -1,29 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-mongo-base
3
- Version: 0.12.2
3
+ Version: 0.12.4
4
4
  Summary: A simple boilerplate application, including models and schemas and abstract router, for FastAPI with MongoDB
5
5
  Author-email: Mahdi Kiani <mahdikiany@gmail.com>
6
6
  Maintainer-email: Mahdi Kiani <mahdikiany@gmail.com>
7
- License: Copyright (c) 2016 The Python Packaging Authority (PyPA)
8
-
9
- Permission is hereby granted, free of charge, to any person obtaining a copy of
10
- this software and associated documentation files (the "Software"), to deal in
11
- the Software without restriction, including without limitation the rights to
12
- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
13
- of the Software, and to permit persons to whom the Software is furnished to do
14
- so, subject to the following conditions:
15
-
16
- The above copyright notice and this permission notice shall be included in all
17
- copies or substantial portions of the Software.
18
-
19
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
- SOFTWARE.
26
-
7
+ License-Expression: MIT
27
8
  Project-URL: Homepage, https://github.com/mahdikiani/fastapi-mongo-base-app
28
9
  Project-URL: Bug Reports, https://github.com/mahdikiani/fastapi-mongo-base-app/issues
29
10
  Project-URL: Funding, https://github.com/mahdikiani/fastapi-mongo-base-app
@@ -33,7 +14,6 @@ Keywords: fastapi,mongodb,beanie
33
14
  Classifier: Development Status :: 3 - Alpha
34
15
  Classifier: Intended Audience :: Developers
35
16
  Classifier: Topic :: Software Development :: Build Tools
36
- Classifier: License :: OSI Approved :: MIT License
37
17
  Classifier: Programming Language :: Python :: 3
38
18
  Classifier: Programming Language :: Python :: 3.10
39
19
  Classifier: Programming Language :: Python :: 3.11
@@ -53,6 +33,8 @@ Requires-Dist: uvicorn[standard]>=0.13.0
53
33
  Provides-Extra: image
54
34
  Requires-Dist: Pillow>=9.0.0; extra == "image"
55
35
  Provides-Extra: test
36
+ Requires-Dist: pytest; extra == "test"
37
+ Requires-Dist: mongomock_motor; extra == "test"
56
38
  Requires-Dist: coverage; extra == "test"
57
39
  Dynamic: license-file
58
40
 
@@ -4,23 +4,19 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fastapi-mongo-base"
7
- version = "0.12.2"
7
+ version = "0.12.4"
8
8
  description = "A simple boilerplate application, including models and schemas and abstract router, for FastAPI with MongoDB"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
11
- license = {file = "LICENSE.txt"}
11
+ license = "MIT"
12
+ license-files = ["LICENSE.txt"]
12
13
  keywords = ["fastapi", "mongodb", "beanie"]
13
- authors = [
14
- {name = "Mahdi Kiani", email = "mahdikiany@gmail.com"}
15
- ]
16
- maintainers = [
17
- {name = "Mahdi Kiani", email = "mahdikiany@gmail.com"}
18
- ]
14
+ authors = [{ name = "Mahdi Kiani", email = "mahdikiany@gmail.com" }]
15
+ maintainers = [{ name = "Mahdi Kiani", email = "mahdikiany@gmail.com" }]
19
16
  classifiers = [
20
17
  "Development Status :: 3 - Alpha",
21
18
  "Intended Audience :: Developers",
22
19
  "Topic :: Software Development :: Build Tools",
23
- "License :: OSI Approved :: MIT License",
24
20
  "Programming Language :: Python :: 3",
25
21
  "Programming Language :: Python :: 3.10",
26
22
  "Programming Language :: Python :: 3.11",
@@ -35,9 +31,18 @@ dependencies = [
35
31
  "uuid6",
36
32
  "pytz",
37
33
  "fastapi>=0.65.0",
38
- "uvicorn[standard]>=0.13.0"
34
+ "uvicorn[standard]>=0.13.0",
35
+ ]
36
+
37
+ [project.optional-dependencies]
38
+ image = [
39
+ "Pillow>=9.0.0",
40
+ ]
41
+ test = [
42
+ "pytest",
43
+ "mongomock_motor",
44
+ "coverage",
39
45
  ]
40
- optional-dependencies = {"image" = ["Pillow>=9.0.0"],"test" = ["coverage"]}
41
46
 
42
47
  [project.urls]
43
48
  "Homepage" = "https://github.com/mahdikiani/fastapi-mongo-base-app"
@@ -47,4 +52,17 @@ optional-dependencies = {"image" = ["Pillow>=9.0.0"],"test" = ["coverage"]}
47
52
  "Source" = "https://github.com/mahdikiani/fastapi-mongo-base-app"
48
53
 
49
54
  [tool.setuptools]
50
- package-data = {"usso" = ["*.dat"]}
55
+ package-data = { "usso" = ["*.dat"] }
56
+
57
+ [tool.ruff]
58
+ line-length = 79
59
+ target-version = "py313"
60
+ fix = true
61
+ unsafe-fixes = true
62
+ preview = true
63
+
64
+ [tool.ruff.lint]
65
+ select = ["E", "F", "W", "I", "UP", "B"]
66
+
67
+ [tool.ruff.format]
68
+ quote-style = "double"
@@ -18,9 +18,15 @@ async def health(request: fastapi.Request):
18
18
 
19
19
  @asynccontextmanager
20
20
  async def lifespan(
21
- *, app: fastapi.FastAPI, worker=None, init_functions=[], settings: Settings = None
21
+ *,
22
+ app: fastapi.FastAPI,
23
+ worker=None,
24
+ init_functions=None,
25
+ settings: Settings = None,
22
26
  ):
23
27
  """Initialize application services."""
28
+ if init_functions is None:
29
+ init_functions = []
24
30
  await db.init_mongo_db()
25
31
 
26
32
  if worker:
@@ -39,7 +45,9 @@ async def lifespan(
39
45
  logging.info("Shutdown complete")
40
46
 
41
47
 
42
- def setup_exception_handlers(*, app: fastapi.FastAPI, handlers: dict = None, **kwargs):
48
+ def setup_exception_handlers(
49
+ *, app: fastapi.FastAPI, handlers: dict = None, **kwargs
50
+ ):
43
51
  exception_handlers = exceptions.EXCEPTION_HANDLERS
44
52
  if handlers:
45
53
  exception_handlers.update(handlers)
@@ -69,14 +77,18 @@ def get_app_kwargs(
69
77
  version="0.1.0",
70
78
  lifespan_func=None,
71
79
  worker=None,
72
- init_functions: list = [],
80
+ init_functions: list = None,
73
81
  contact=None,
74
- license_info={
75
- "name": "MIT License",
76
- "url": "https://github.com/mahdikiani/FastAPILaunchpad/blob/main/LICENSE",
77
- },
82
+ license_info=None,
78
83
  **kwargs,
79
84
  ):
85
+ if license_info is None:
86
+ license_info = {
87
+ "name": "MIT License",
88
+ "url": "https://github.com/mahdikiani/FastAPILaunchpad/blob/main/LICENSE",
89
+ }
90
+ if init_functions is None:
91
+ init_functions = []
80
92
  settings.config_logger()
81
93
 
82
94
  """Create a FastAPI app with shared configurations."""
@@ -92,9 +104,14 @@ def get_app_kwargs(
92
104
  base_path: str = settings.base_path
93
105
 
94
106
  if lifespan_func is None:
95
- lifespan_func = lambda app: lifespan(
96
- app=app, worker=worker, init_functions=init_functions, settings=settings
97
- )
107
+
108
+ def lifespan_func(app):
109
+ return lifespan(
110
+ app=app,
111
+ worker=worker,
112
+ init_functions=init_functions,
113
+ settings=settings,
114
+ )
98
115
 
99
116
  docs_url = f"{base_path}/docs"
100
117
  openapi_url = f"{base_path}/openapi.json"
@@ -122,18 +139,22 @@ def create_app(
122
139
  origins: list = None,
123
140
  lifespan_func=None,
124
141
  worker=None,
125
- init_functions: list = [],
142
+ init_functions: list = None,
126
143
  contact=None,
127
- license_info={
128
- "name": "MIT License",
129
- "url": "https://github.com/mahdikiani/FastAPILaunchpad/blob/main/LICENSE",
130
- },
144
+ license_info=None,
131
145
  exception_handlers: dict = None,
132
146
  log_route: bool = False,
133
147
  health_route: bool = True,
134
148
  index_route: bool = True,
135
149
  **kwargs,
136
150
  ) -> fastapi.FastAPI:
151
+ if license_info is None:
152
+ license_info = {
153
+ "name": "MIT License",
154
+ "url": "https://github.com/mahdikiani/FastAPILaunchpad/blob/main/LICENSE",
155
+ }
156
+ if init_functions is None:
157
+ init_functions = []
137
158
  data = get_app_kwargs(
138
159
  settings=settings,
139
160
  title=title,
@@ -197,7 +218,7 @@ def configure_app(
197
218
  if log_route:
198
219
  app.get(f"{base_path}/logs", include_in_schema=False)(logs)
199
220
  if index_route:
200
- app.get(f"/", include_in_schema=False)(index)
221
+ app.get("/", include_in_schema=False)(index)
201
222
 
202
223
  if serve_coverage:
203
224
  app.mount(
@@ -36,7 +36,7 @@ class Settings(metaclass=Singleton):
36
36
  log_config = {
37
37
  "formatters": {
38
38
  "standard": {
39
- "format": "[{levelname} : {filename}:{lineno} : {asctime} -> {funcName:10}] {message}",
39
+ "format": "[{levelname} : {filename}:{lineno} : {asctime} -> {funcName:10}] {message}", # noqa: E501
40
40
  "style": "{",
41
41
  }
42
42
  },
@@ -34,10 +34,10 @@ def init_redis():
34
34
  from redis import Redis as RedisSync
35
35
  from redis.asyncio.client import Redis
36
36
 
37
- if getattr(Settings, "redis_uri"):
37
+ if Settings.redis_uri:
38
38
  redis_sync: RedisSync = RedisSync.from_url(Settings.redis_uri)
39
39
  redis: Redis = Redis.from_url(Settings.redis_uri)
40
- except ImportError or AttributeError or Exception as e:
40
+ except (ImportError, AttributeError, Exception) as e:
41
41
  logging.error(f"Error initializing Redis: {e}")
42
42
  redis_sync = None
43
43
  redis = None
@@ -1,7 +1,7 @@
1
- from enum import Enum
1
+ from enum import StrEnum
2
2
 
3
3
 
4
- class Language(str, Enum):
4
+ class Language(StrEnum):
5
5
  English = "English"
6
6
  Persian = "Persian"
7
7
  Arabic = "Arabic"
@@ -36,7 +36,9 @@ class BaseHTTPException(HTTPException):
36
36
  super().__init__(status_code, detail=detail)
37
37
 
38
38
 
39
- async def base_http_exception_handler(request: Request, exc: BaseHTTPException):
39
+ async def base_http_exception_handler(
40
+ request: Request, exc: BaseHTTPException
41
+ ):
40
42
  return JSONResponse(
41
43
  status_code=exc.status_code,
42
44
  content={"message": exc.message, "error": exc.error},
@@ -58,7 +60,8 @@ async def request_validation_exception_handler(
58
60
  request: Request, exc: RequestValidationError
59
61
  ):
60
62
  logging.error(
61
- f"request_validation_exception: {request.url} {exc}\n{(await request.body())[:100]}"
63
+ f"request_validation_exception: {request.url} {exc}\n"
64
+ f"{(await request.body())[:100]}"
62
65
  )
63
66
  from fastapi.exception_handlers import (
64
67
  request_validation_exception_handler as default_handler,
@@ -1,6 +1,14 @@
1
1
  from datetime import datetime
2
2
 
3
- from beanie import Document, Insert, Replace, Save, SaveChanges, Update, before_event
3
+ from beanie import (
4
+ Document,
5
+ Insert,
6
+ Replace,
7
+ Save,
8
+ SaveChanges,
9
+ Update,
10
+ before_event,
11
+ )
4
12
  from beanie.odm.queries.find import FindMany
5
13
  from pydantic import ConfigDict
6
14
  from pymongo import ASCENDING, IndexModel
@@ -29,8 +37,11 @@ class BaseEntity(BaseEntitySchema, Document):
29
37
 
30
38
  @classmethod
31
39
  def is_abstract(cls):
32
- # Use `__dict__` to check if `__abstract__` is defined in the class itself
33
- return "__abstract__" in cls.__dict__ and cls.__dict__["__abstract__"]
40
+ # Use `__dict__` to check if `__abstract__` is defined
41
+ # in the class itself
42
+ return (
43
+ "__abstract__" in cls.__dict__ and cls.__dict__["__abstract__"]
44
+ )
34
45
 
35
46
  @before_event([Insert, Replace, Save, SaveChanges, Update])
36
47
  async def pre_save(self):
@@ -39,13 +50,13 @@ class BaseEntity(BaseEntitySchema, Document):
39
50
  @classmethod
40
51
  def get_queryset(
41
52
  cls,
53
+ *,
42
54
  user_id: str | None = None,
43
55
  tenant_id: str | None = None,
44
56
  is_deleted: bool = False,
45
57
  uid: str | None = None,
46
- *args,
47
58
  **kwargs,
48
- ) -> list[dict]:
59
+ ) -> dict:
49
60
  """Build a MongoDB query filter based on provided parameters.
50
61
 
51
62
  Args:
@@ -53,25 +64,26 @@ class BaseEntity(BaseEntitySchema, Document):
53
64
  tenant_id: Filter by tenant ID if the model has tenant_id field
54
65
  is_deleted: Filter by deletion status
55
66
  uid: Filter by unique identifier
56
- **kwargs: Additional filters that can include range queries with _from/_to suffixes
67
+ **kwargs: Additional filters that can include range queries
68
+ with _from/_to suffixes
57
69
 
58
70
  Returns:
59
71
  List of MongoDB query conditions
60
72
  """
61
73
  # Start with basic filters
62
- base_query = []
74
+ base_query = {}
63
75
 
64
76
  # Add standard filters if applicable
65
- base_query.append({"is_deleted": is_deleted})
66
-
67
- if hasattr(cls, "user_id") and user_id:
68
- base_query.append({"user_id": user_id})
77
+ base_query.update({"is_deleted": is_deleted})
69
78
 
70
79
  if hasattr(cls, "tenant_id"):
71
- base_query.append({"tenant_id": tenant_id})
80
+ base_query.update({"tenant_id": tenant_id})
81
+
82
+ if hasattr(cls, "user_id") and user_id:
83
+ base_query.update({"user_id": user_id})
72
84
 
73
85
  if uid:
74
- base_query.append({"uid": uid})
86
+ base_query.update({"uid": uid})
75
87
 
76
88
  # Process additional filters from kwargs
77
89
  for key, value in kwargs.items():
@@ -82,9 +94,15 @@ class BaseEntity(BaseEntitySchema, Document):
82
94
  base_field = basic.get_base_field_name(key)
83
95
 
84
96
  # Validate field is allowed for searching
85
- if cls.search_field_set() and base_field not in cls.search_field_set():
97
+ if (
98
+ cls.search_field_set()
99
+ and base_field not in cls.search_field_set()
100
+ ):
86
101
  continue
87
- if cls.search_exclude_set() and base_field in cls.search_exclude_set():
102
+ if (
103
+ cls.search_exclude_set()
104
+ and base_field in cls.search_exclude_set()
105
+ ):
88
106
  continue
89
107
  if not hasattr(cls, base_field):
90
108
  continue
@@ -92,29 +110,27 @@ class BaseEntity(BaseEntitySchema, Document):
92
110
  # Handle range queries and array operators
93
111
  if key.endswith("_from") or key.endswith("_to"):
94
112
  if basic.is_valid_range_value(value):
95
- if key.endswith("_from"):
96
- base_query.append({base_field: {"$gte": value}})
97
- elif key.endswith("_to"):
98
- base_query.append({base_field: {"$lte": value}})
113
+ op = "$gte" if key.endswith("_from") else "$lte"
114
+ base_query.setdefault(base_field, {}).update({op: value})
99
115
  elif key.endswith("_in") or key.endswith("_nin"):
100
116
  value_list = basic.parse_array_parameter(value)
101
117
  operator = "$in" if key.endswith("_in") else "$nin"
102
- base_query.append({base_field: {operator: value_list}})
118
+ base_query.update({base_field: {operator: value_list}})
103
119
  else:
104
- base_query.append({key: value})
120
+ base_query.update({key: value})
105
121
 
106
122
  return base_query
107
123
 
108
124
  @classmethod
109
125
  def get_query(
110
126
  cls,
127
+ *,
111
128
  user_id: str | None = None,
112
129
  tenant_id: str | None = None,
113
130
  is_deleted: bool = False,
114
131
  uid: str | None = None,
115
132
  created_at_from: datetime = None,
116
133
  created_at_to: datetime = None,
117
- *args,
118
134
  **kwargs,
119
135
  ) -> FindMany:
120
136
  base_query = cls.get_queryset(
@@ -124,7 +140,6 @@ class BaseEntity(BaseEntitySchema, Document):
124
140
  uid=uid,
125
141
  created_at_from=created_at_from,
126
142
  created_at_to=created_at_to,
127
- *args,
128
143
  **kwargs,
129
144
  )
130
145
  query = cls.find({"$and": base_query})
@@ -145,7 +160,7 @@ class BaseEntity(BaseEntitySchema, Document):
145
160
  tenant_id=tenant_id,
146
161
  is_deleted=is_deleted,
147
162
  uid=uid,
148
- *args,
163
+ *args, # noqa: B026
149
164
  **kwargs,
150
165
  )
151
166
  items = await query.to_list()
@@ -185,7 +200,6 @@ class BaseEntity(BaseEntitySchema, Document):
185
200
  user_id=user_id,
186
201
  tenant_id=tenant_id,
187
202
  is_deleted=is_deleted,
188
- *args,
189
203
  **kwargs,
190
204
  )
191
205
 
@@ -206,7 +220,7 @@ class BaseEntity(BaseEntitySchema, Document):
206
220
  user_id=user_id,
207
221
  tenant_id=tenant_id,
208
222
  is_deleted=is_deleted,
209
- *args,
223
+ *args, # noqa: B026
210
224
  **kwargs,
211
225
  )
212
226
  return await query.count()
@@ -285,55 +299,86 @@ class BaseEntity(BaseEntitySchema, Document):
285
299
 
286
300
 
287
301
  class UserOwnedEntity(UserOwnedEntitySchema, BaseEntity):
288
-
289
302
  class Settings(BaseEntity.Settings):
290
303
  __abstract__ = True
291
304
 
292
- indexes = BaseEntity.Settings.indexes + [IndexModel([("user_id", ASCENDING)])]
305
+ indexes = BaseEntity.Settings.indexes + [
306
+ IndexModel([("user_id", ASCENDING), ("uid", ASCENDING)]),
307
+ ]
293
308
 
294
309
  @classmethod
295
- async def get_item(cls, uid, user_id, *args, **kwargs) -> "UserOwnedEntity":
296
- if user_id == None and kwargs.get("ignore_user_id") != True:
310
+ async def get_item(
311
+ cls,
312
+ uid,
313
+ user_id,
314
+ *args,
315
+ **kwargs,
316
+ ) -> "UserOwnedEntity":
317
+ if user_id is None and not kwargs.get("ignore_user_id"):
297
318
  raise ValueError("user_id is required")
298
- return await super().get_item(uid, user_id=user_id, *args, **kwargs)
319
+ return await super().get_item(
320
+ uid,
321
+ user_id=user_id,
322
+ *args, # noqa: B026
323
+ **kwargs,
324
+ )
299
325
 
300
326
 
301
327
  class TenantScopedEntity(TenantScopedEntitySchema, BaseEntity):
302
-
303
328
  class Settings(BaseEntity.Settings):
304
329
  __abstract__ = True
305
330
 
306
- indexes = BaseEntity.Settings.indexes + [IndexModel([("tenant_id", ASCENDING)])]
331
+ indexes = BaseEntity.Settings.indexes + [
332
+ IndexModel([("tenant_id", ASCENDING), ("uid", ASCENDING)]),
333
+ ]
307
334
 
308
335
  @classmethod
309
- async def get_item(cls, uid, tenant_id, *args, **kwargs) -> "TenantScopedEntity":
310
- if tenant_id == None:
336
+ async def get_item(
337
+ cls,
338
+ uid,
339
+ tenant_id,
340
+ *args,
341
+ **kwargs,
342
+ ) -> "TenantScopedEntity":
343
+ if tenant_id is None:
311
344
  raise ValueError("tenant_id is required")
312
- return await super().get_item(uid, tenant_id=tenant_id, *args, **kwargs)
345
+ return await super().get_item(
346
+ uid,
347
+ tenant_id=tenant_id,
348
+ *args, # noqa: B026
349
+ **kwargs,
350
+ )
313
351
 
314
352
  async def get_tenant(self):
315
353
  raise NotImplementedError
316
354
 
317
355
 
318
356
  class TenantUserEntity(TenantUserEntitySchema, BaseEntity):
319
-
320
357
  class Settings(TenantScopedEntity.Settings):
321
358
  __abstract__ = True
322
359
 
323
- indexes = TenantScopedEntity.Settings.indexes + [
324
- IndexModel([("user_id", ASCENDING)])
360
+ indexes = UserOwnedEntity.Settings.indexes + [
361
+ IndexModel([
362
+ ("tenant_id", ASCENDING),
363
+ ("user_id", ASCENDING),
364
+ ("uid", ASCENDING),
365
+ ]),
325
366
  ]
326
367
 
327
368
  @classmethod
328
369
  async def get_item(
329
370
  cls, uid, tenant_id, user_id, *args, **kwargs
330
371
  ) -> "TenantUserEntity":
331
- if tenant_id == None:
372
+ if tenant_id is None:
332
373
  raise ValueError("tenant_id is required")
333
374
  # if user_id == None:
334
375
  # raise ValueError("user_id is required")
335
376
  return await super().get_item(
336
- uid, tenant_id=tenant_id, user_id=user_id, *args, **kwargs
377
+ uid,
378
+ tenant_id=tenant_id,
379
+ user_id=user_id,
380
+ *args, # noqa: B026
381
+ **kwargs,
337
382
  )
338
383
 
339
384
 
@@ -1,6 +1,6 @@
1
1
  import asyncio
2
2
  from datetime import datetime
3
- from typing import Any, Type, TypeVar
3
+ from typing import Any, TypeVar
4
4
 
5
5
  import singleton
6
6
  from fastapi import APIRouter, BackgroundTasks, Query, Request
@@ -21,15 +21,14 @@ TS = TypeVar("TS", bound=BaseEntitySchema)
21
21
 
22
22
 
23
23
  class AbstractBaseRouter(metaclass=singleton.Singleton):
24
-
25
24
  def __init__(
26
25
  self,
27
- model: Type[T],
26
+ model: type[T],
28
27
  *args,
29
28
  user_dependency=None,
30
29
  prefix: str = None,
31
30
  tags: list[str] = None,
32
- schema: Type[TS] = None,
31
+ schema: type[TS] = None,
33
32
  **kwargs,
34
33
  ):
35
34
  self.model = model
@@ -52,13 +51,25 @@ class AbstractBaseRouter(metaclass=singleton.Singleton):
52
51
  self.list_response_schema = kwargs.get(
53
52
  "list_response_schema", PaginatedResponse[self.list_item_schema]
54
53
  )
55
- self.retrieve_response_schema = kwargs.get("retrieve_response_schema", schema)
56
- self.create_response_schema = kwargs.get("create_response_schema", schema)
57
- self.update_response_schema = kwargs.get("update_response_schema", schema)
58
- self.delete_response_schema = kwargs.get("delete_response_schema", schema)
54
+ self.retrieve_response_schema = kwargs.get(
55
+ "retrieve_response_schema", schema
56
+ )
57
+ self.create_response_schema = kwargs.get(
58
+ "create_response_schema", schema
59
+ )
60
+ self.update_response_schema = kwargs.get(
61
+ "update_response_schema", schema
62
+ )
63
+ self.delete_response_schema = kwargs.get(
64
+ "delete_response_schema", schema
65
+ )
59
66
 
60
- self.create_request_schema = kwargs.get("create_request_schema", schema)
61
- self.update_request_schema = kwargs.get("update_request_schema", schema)
67
+ self.create_request_schema = kwargs.get(
68
+ "create_request_schema", schema
69
+ )
70
+ self.update_request_schema = kwargs.get(
71
+ "update_request_schema", schema
72
+ )
62
73
 
63
74
  def config_routes(self, **kwargs):
64
75
  prefix: str = kwargs.get("prefix", "")
@@ -174,7 +185,9 @@ class AbstractBaseRouter(metaclass=singleton.Singleton):
174
185
  limit=limit,
175
186
  **kwargs,
176
187
  )
177
- items_in_schema = [self.list_item_schema(**item.model_dump()) for item in items]
188
+ items_in_schema = [
189
+ self.list_item_schema(**item.model_dump()) for item in items
190
+ ]
178
191
 
179
192
  return PaginatedResponse(
180
193
  items=items_in_schema,
@@ -244,7 +257,7 @@ class AbstractBaseRouter(metaclass=singleton.Singleton):
244
257
  class AbstractTaskRouter(AbstractBaseRouter):
245
258
  def __init__(
246
259
  self,
247
- model: Type[T],
260
+ model: type[T],
248
261
  user_dependency: Any,
249
262
  schema: TS,
250
263
  draftable: bool = True,
@@ -252,7 +265,13 @@ class AbstractTaskRouter(AbstractBaseRouter):
252
265
  **kwargs,
253
266
  ):
254
267
  self.draftable = draftable
255
- super().__init__(model, user_dependency, schema=schema, *args, **kwargs)
268
+ super().__init__(
269
+ model,
270
+ user_dependency,
271
+ schema=schema,
272
+ *args, # noqa: B026
273
+ **kwargs,
274
+ )
256
275
 
257
276
  def config_routes(self, **kwargs):
258
277
  super().config_routes(**kwargs)
@@ -1,5 +1,5 @@
1
1
  from datetime import datetime
2
- from typing import Generic, TypeVar
2
+ from typing import TypeVar
3
3
 
4
4
  import uuid6
5
5
  from pydantic import BaseModel, Field
@@ -71,10 +71,6 @@ class BaseEntitySchema(BaseModel):
71
71
  class UserOwnedEntitySchema(BaseEntitySchema):
72
72
  user_id: str
73
73
 
74
- @classmethod
75
- def create_exclude_set(cls) -> list[str]:
76
- return super().create_exclude_set() + ["user_id"]
77
-
78
74
  @classmethod
79
75
  def update_exclude_set(cls) -> list[str]:
80
76
  return super().update_exclude_set() + ["user_id"]
@@ -83,30 +79,23 @@ class UserOwnedEntitySchema(BaseEntitySchema):
83
79
  class TenantScopedEntitySchema(BaseEntitySchema):
84
80
  tenant_id: str
85
81
 
86
- @classmethod
87
- def create_exclude_set(cls) -> list[str]:
88
- return super().create_exclude_set() + ["tenant_id"]
89
-
90
82
  @classmethod
91
83
  def update_exclude_set(cls) -> list[str]:
92
84
  return super().update_exclude_set() + ["tenant_id"]
93
85
 
94
86
 
95
87
  class TenantUserEntitySchema(TenantScopedEntitySchema, UserOwnedEntitySchema):
96
-
97
- @classmethod
98
- def create_exclude_set(cls) -> list[str]:
99
- return list(set(super().create_exclude_set() + ["tenant_id", "user_id"]))
100
-
101
88
  @classmethod
102
89
  def update_exclude_set(cls) -> list[str]:
103
- return list(set(super().update_exclude_set() + ["tenant_id", "user_id"]))
90
+ return list(
91
+ set(super().update_exclude_set() + ["tenant_id", "user_id"])
92
+ )
104
93
 
105
94
 
106
95
  T = TypeVar("T", bound=BaseEntitySchema)
107
96
 
108
97
 
109
- class PaginatedResponse(BaseModel, Generic[T]):
98
+ class PaginatedResponse[T: BaseEntitySchema](BaseModel):
110
99
  items: list[T]
111
100
  total: int
112
101
  offset: int
@@ -68,10 +68,13 @@ class BaseEntity:
68
68
  return (datetime.now(timezone.tz) - self.updated_at).days > days
69
69
 
70
70
  def dump(
71
- self, include_fields: list[str] = None, exclude_fields: list[str] = None
71
+ self,
72
+ include_fields: list[str] = None,
73
+ exclude_fields: list[str] = None,
72
74
  ) -> dict:
73
75
  """
74
- Dump the object into a dictionary. It includes all the fields of the object.
76
+ Dump the object into a dictionary.
77
+ It includes all the fields of the object.
75
78
  """
76
79
  result = {}
77
80
  for key, value in (include_fields or self.__dict__).items():
@@ -112,7 +115,8 @@ class BaseEntity:
112
115
  tenant_id: Filter by tenant ID if the model has tenant_id field
113
116
  is_deleted: Filter by deletion status
114
117
  uid: Filter by unique identifier
115
- **kwargs: Additional filters that can include range queries with _from/_to suffixes
118
+ **kwargs: Additional filters that can include range queries
119
+ with _from/_to suffixes
116
120
 
117
121
  Returns:
118
122
  List of SQLAlchemy query conditions
@@ -141,9 +145,15 @@ class BaseEntity:
141
145
  base_field = basic.get_base_field_name(key)
142
146
 
143
147
  # Validate field is allowed for searching
144
- if cls.search_field_set() and base_field not in cls.search_field_set():
148
+ if (
149
+ cls.search_field_set()
150
+ and base_field not in cls.search_field_set()
151
+ ):
145
152
  continue
146
- if cls.search_exclude_set() and base_field in cls.search_exclude_set():
153
+ if (
154
+ cls.search_exclude_set()
155
+ and base_field in cls.search_exclude_set()
156
+ ):
147
157
  continue
148
158
  if not hasattr(cls, base_field):
149
159
  continue
@@ -177,7 +187,6 @@ class BaseEntity:
177
187
  uid: str | None = None,
178
188
  created_at_from: datetime | None = None,
179
189
  created_at_to: datetime | None = None,
180
- *args,
181
190
  **kwargs,
182
191
  ):
183
192
  base_query = cls.get_queryset(
@@ -187,7 +196,6 @@ class BaseEntity:
187
196
  uid=uid,
188
197
  created_at_from=created_at_from,
189
198
  created_at_to=created_at_to,
190
- *args,
191
199
  **kwargs,
192
200
  )
193
201
  return base_query
@@ -225,7 +233,6 @@ class BaseEntity:
225
233
  limit: int = 10,
226
234
  **kwargs,
227
235
  ):
228
-
229
236
  base_query = cls.get_query(
230
237
  user_id=user_id,
231
238
  tenant_id=tenant_id,
@@ -254,7 +261,6 @@ class BaseEntity:
254
261
  is_deleted: bool = False,
255
262
  **kwargs,
256
263
  ):
257
-
258
264
  base_query = cls.get_query(
259
265
  user_id=user_id,
260
266
  tenant_id=tenant_id,
@@ -263,7 +269,9 @@ class BaseEntity:
263
269
  )
264
270
 
265
271
  # Query for getting the total count of items
266
- total_count_query = select(func.count()).filter(*base_query) # .subquery()
272
+ total_count_query = select(func.count()).filter(
273
+ *base_query
274
+ ) # .subquery()
267
275
 
268
276
  async with async_session() as session:
269
277
  total_result = await session.execute(total_count_query)
@@ -299,7 +307,6 @@ class BaseEntity:
299
307
 
300
308
  @classmethod
301
309
  async def get_by_uid(cls, uid: str):
302
-
303
310
  async with async_session() as session:
304
311
  query = select(cls).filter(cls.uid == uid)
305
312
  result = await session.execute(query)
@@ -308,7 +315,6 @@ class BaseEntity:
308
315
 
309
316
  @classmethod
310
317
  async def create_item(cls, data: dict):
311
-
312
318
  item = cls(**data)
313
319
  async with async_session() as session:
314
320
  session.add(item)
@@ -318,7 +324,6 @@ class BaseEntity:
318
324
 
319
325
  @classmethod
320
326
  async def update_item(cls, item: "BaseEntity", data: dict):
321
-
322
327
  for key, value in data.items():
323
328
  if cls.update_field_set() and key not in cls.update_field_set():
324
329
  continue
@@ -335,7 +340,6 @@ class BaseEntity:
335
340
 
336
341
  @classmethod
337
342
  async def delete_item(cls, item: "BaseEntity"):
338
-
339
343
  item.is_deleted = True
340
344
  async with async_session() as session:
341
345
  session.add(item)
@@ -377,11 +381,15 @@ class TenantUserEntity(TenantScopedEntity, UserOwnedEntity):
377
381
 
378
382
  @classmethod
379
383
  def create_exclude_set(cls) -> list[str]:
380
- return list(set(super().create_exclude_set() + ["tenant_id", "user_id"]))
384
+ return list(
385
+ set(super().create_exclude_set() + ["tenant_id", "user_id"])
386
+ )
381
387
 
382
388
  @classmethod
383
389
  def update_exclude_set(cls) -> list[str]:
384
- return list(set(super().update_exclude_set() + ["tenant_id", "user_id"]))
390
+ return list(
391
+ set(super().update_exclude_set() + ["tenant_id", "user_id"])
392
+ )
385
393
 
386
394
 
387
395
  class ImmutableMixin(BaseEntity):
@@ -1,8 +1,9 @@
1
1
  import asyncio
2
2
  import logging
3
+ from collections.abc import Callable, Coroutine
3
4
  from datetime import datetime
4
- from enum import Enum
5
- from typing import Any, Callable, Coroutine, Literal, Union
5
+ from enum import StrEnum
6
+ from typing import Any, Literal, Union
6
7
 
7
8
  import json_advanced as json
8
9
  from pydantic import BaseModel, Field, field_serializer, field_validator
@@ -13,7 +14,7 @@ from .schemas import BaseEntitySchema
13
14
  from .utils import basic
14
15
 
15
16
 
16
- class TaskStatusEnum(str, Enum):
17
+ class TaskStatusEnum(StrEnum):
17
18
  none = "null"
18
19
  draft = "draft"
19
20
  init = "init"
@@ -36,12 +37,16 @@ class SignalRegistry(metaclass=Singleton):
36
37
  def __init__(self):
37
38
  self.signal_map: dict[
38
39
  str,
39
- list[Callable[..., None] | Callable[..., Coroutine[Any, Any, None]]],
40
+ list[
41
+ Callable[..., None] | Callable[..., Coroutine[Any, Any, None]]
42
+ ],
40
43
  ] = {}
41
44
 
42
45
 
43
46
  class TaskLogRecord(BaseModel):
44
- reported_at: datetime = Field(default_factory=lambda: datetime.now(timezone.tz))
47
+ reported_at: datetime = Field(
48
+ default_factory=lambda: datetime.now(timezone.tz)
49
+ )
45
50
  message: str
46
51
  task_status: TaskStatusEnum
47
52
  duration: int = 0
@@ -60,7 +65,12 @@ class TaskLogRecord(BaseModel):
60
65
  return False
61
66
 
62
67
  def __hash__(self):
63
- return hash((self.reported_at, self.message, self.task_status, self.duration))
68
+ return hash((
69
+ self.reported_at,
70
+ self.message,
71
+ self.task_status,
72
+ self.duration,
73
+ ))
64
74
 
65
75
 
66
76
  class TaskReference(BaseModel):
@@ -69,7 +79,10 @@ class TaskReference(BaseModel):
69
79
 
70
80
  def __eq__(self, other):
71
81
  if isinstance(other, TaskReference):
72
- return self.task_id == other.task_id and self.task_type == other.task_type
82
+ return (
83
+ self.task_id == other.task_id
84
+ and self.task_type == other.task_type
85
+ )
73
86
  return False
74
87
 
75
88
  def __hash__(self):
@@ -90,7 +103,8 @@ class TaskReference(BaseModel):
90
103
  task_item = await task_class.find_one(task_class.uid == self.task_id)
91
104
  if not task_item:
92
105
  raise ValueError(
93
- f"No task found with id {self.task_id} of type {self.task_type}."
106
+ f"No task found with id {self.task_id} of type "
107
+ f"{self.task_type}."
94
108
  )
95
109
 
96
110
  return task_item
@@ -107,7 +121,9 @@ class TaskReferenceList(BaseModel):
107
121
  for task_item in task_items:
108
122
  await task_item.start_processing()
109
123
  case "parallel":
110
- await asyncio.gather(*[task.start_processing() for task in task_items])
124
+ await asyncio.gather(*[
125
+ task.start_processing() for task in task_items
126
+ ])
111
127
 
112
128
 
113
129
  class TaskMixin(BaseModel):
@@ -165,8 +181,9 @@ class TaskMixin(BaseModel):
165
181
  cls.signals().append(signal)
166
182
 
167
183
  @classmethod
168
- async def emit_signals(cls, task_instance: "TaskMixin", *, sync=False, **kwargs):
169
-
184
+ async def emit_signals(
185
+ cls, task_instance: "TaskMixin", *, sync=False, **kwargs
186
+ ):
170
187
  async def webhook_call(*args, **kwargs):
171
188
  import httpx
172
189
 
@@ -183,16 +200,18 @@ class TaskMixin(BaseModel):
183
200
  )
184
201
  await task_instance.save()
185
202
  logging.error(
186
- "\n".join(
187
- ["An error occurred in webhook_call:", traceback_str, str(e)]
188
- )
203
+ "\n".join([
204
+ "An error occurred in webhook_call:",
205
+ traceback_str,
206
+ str(e),
207
+ ])
189
208
  )
190
209
 
191
210
  def webhook_task(webhook_url: str):
192
211
  return
193
212
 
194
213
  signals = []
195
- meta_data = getattr(task_instance, "meta_data") or {}
214
+ meta_data = task_instance.meta_data or {}
196
215
  task_dict = task_instance.model_dump()
197
216
  task_dict.update({"task_type": task_instance.__class__.__name__})
198
217
  task_dict.update(kwargs)
@@ -269,7 +288,9 @@ class TaskMixin(BaseModel):
269
288
  **kwargs,
270
289
  )
271
290
 
272
- async def add_log(self, log_record: TaskLogRecord, *, emit: bool = True, **kwargs):
291
+ async def add_log(
292
+ self, log_record: TaskLogRecord, *, emit: bool = True, **kwargs
293
+ ):
273
294
  self.task_logs.append(log_record)
274
295
  if emit:
275
296
  # await self.emit_signals(self)
@@ -277,7 +298,9 @@ class TaskMixin(BaseModel):
277
298
 
278
299
  async def start_processing(self, **kwargs):
279
300
  if self.task_references is None:
280
- raise NotImplementedError("Subclasses should implement this method")
301
+ raise NotImplementedError(
302
+ "Subclasses should implement this method"
303
+ )
281
304
 
282
305
  await self.task_references.list_processing()
283
306
 
@@ -297,7 +320,9 @@ class TaskMixin(BaseModel):
297
320
  await self.save()
298
321
  await self.emit_signals(self, **kwargs)
299
322
  else:
300
- await asyncio.gather(self.save(), self.emit_signals(self, **kwargs))
323
+ await asyncio.gather(
324
+ self.save(), self.emit_signals(self, **kwargs)
325
+ )
301
326
 
302
327
  async def update_and_emit(self, **kwargs):
303
328
  if kwargs.get("task_status") in [
@@ -81,7 +81,8 @@ def try_except_wrapper(func, sync_to_thread=False):
81
81
  func_name = f"{class_name}.{func_name}"
82
82
  traceback_str = "".join(traceback.format_tb(e.__traceback__))
83
83
  logging.error(
84
- f"An error occurred in {func_name} ({args=}, {kwargs=}):\n{traceback_str}\n{type(e)}: {e}"
84
+ f"An error occurred in {func_name} ({args=}, {kwargs=}):\n"
85
+ f"{traceback_str}\n{type(e)}: {e}"
85
86
  )
86
87
  return None
87
88
 
@@ -140,12 +141,15 @@ def retry_execution(attempts, delay=0, sync_to_thread=False):
140
141
  except Exception as e:
141
142
  last_exception = e
142
143
  logging.warning(
143
- f"Attempt {attempt + 1} failed for {func.__name__}: {e}"
144
+ f"Attempt {attempt + 1} failed for {func.__name__}: "
145
+ f"{e}"
144
146
  )
145
147
  if delay > 0 and attempt < attempts - 1:
146
148
  await asyncio.sleep(delay)
147
149
  # If the loop finishes and the function didn't return successfully
148
- logging.error(f"All {attempts} attempts failed for {func.__name__}")
150
+ logging.error(
151
+ f"All {attempts} attempts failed for {func.__name__}"
152
+ )
149
153
  raise last_exception
150
154
 
151
155
  @functools.wraps(func)
@@ -157,12 +161,15 @@ def retry_execution(attempts, delay=0, sync_to_thread=False):
157
161
  except Exception as e:
158
162
  last_exception = e
159
163
  logging.warning(
160
- f"Attempt {attempt + 1} failed for {func.__name__}: {e}"
164
+ f"Attempt {attempt + 1} failed for {func.__name__}: "
165
+ f"{e}"
161
166
  )
162
167
  if delay > 0 and attempt < attempts - 1:
163
168
  time.sleep(delay)
164
169
  # If the loop finishes and the function didn't return successfully
165
- logging.error(f"All {attempts} attempts failed for {func.__name__}")
170
+ logging.error(
171
+ f"All {attempts} attempts failed for {func.__name__}"
172
+ )
166
173
  raise last_exception
167
174
 
168
175
  if sync_to_thread or asyncio.iscoroutinefunction(func):
@@ -6,7 +6,7 @@ from bson.decimal128 import Decimal128
6
6
 
7
7
 
8
8
  def decimal_amount(value):
9
- if type(value) == Decimal128:
9
+ if isinstance(value, Decimal128):
10
10
  return Decimal(value.to_decimal())
11
11
  return value
12
12
 
@@ -1,29 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-mongo-base
3
- Version: 0.12.2
3
+ Version: 0.12.4
4
4
  Summary: A simple boilerplate application, including models and schemas and abstract router, for FastAPI with MongoDB
5
5
  Author-email: Mahdi Kiani <mahdikiany@gmail.com>
6
6
  Maintainer-email: Mahdi Kiani <mahdikiany@gmail.com>
7
- License: Copyright (c) 2016 The Python Packaging Authority (PyPA)
8
-
9
- Permission is hereby granted, free of charge, to any person obtaining a copy of
10
- this software and associated documentation files (the "Software"), to deal in
11
- the Software without restriction, including without limitation the rights to
12
- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
13
- of the Software, and to permit persons to whom the Software is furnished to do
14
- so, subject to the following conditions:
15
-
16
- The above copyright notice and this permission notice shall be included in all
17
- copies or substantial portions of the Software.
18
-
19
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
- SOFTWARE.
26
-
7
+ License-Expression: MIT
27
8
  Project-URL: Homepage, https://github.com/mahdikiani/fastapi-mongo-base-app
28
9
  Project-URL: Bug Reports, https://github.com/mahdikiani/fastapi-mongo-base-app/issues
29
10
  Project-URL: Funding, https://github.com/mahdikiani/fastapi-mongo-base-app
@@ -33,7 +14,6 @@ Keywords: fastapi,mongodb,beanie
33
14
  Classifier: Development Status :: 3 - Alpha
34
15
  Classifier: Intended Audience :: Developers
35
16
  Classifier: Topic :: Software Development :: Build Tools
36
- Classifier: License :: OSI Approved :: MIT License
37
17
  Classifier: Programming Language :: Python :: 3
38
18
  Classifier: Programming Language :: Python :: 3.10
39
19
  Classifier: Programming Language :: Python :: 3.11
@@ -53,6 +33,8 @@ Requires-Dist: uvicorn[standard]>=0.13.0
53
33
  Provides-Extra: image
54
34
  Requires-Dist: Pillow>=9.0.0; extra == "image"
55
35
  Provides-Extra: test
36
+ Requires-Dist: pytest; extra == "test"
37
+ Requires-Dist: mongomock_motor; extra == "test"
56
38
  Requires-Dist: coverage; extra == "test"
57
39
  Dynamic: license-file
58
40
 
@@ -1,4 +1,5 @@
1
1
  LICENSE.txt
2
+ MANIFEST.in
2
3
  README.md
3
4
  pyproject.toml
4
5
  src/fastapi_mongo_base/__init__.py
@@ -11,4 +11,6 @@ uvicorn[standard]>=0.13.0
11
11
  Pillow>=9.0.0
12
12
 
13
13
  [test]
14
+ pytest
15
+ mongomock_motor
14
16
  coverage