fastapi-mongo-base 1.5.2__tar.gz → 1.5.3__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_mongo_base-1.5.2/src/fastapi_mongo_base.egg-info → fastapi_mongo_base-1.5.3}/PKG-INFO +1 -1
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/pyproject.toml +2 -2
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/pytest.ini +1 -1
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/errors/__init__.py +26 -12
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/errors/exceptions.py +1 -1
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/errors/mongodb.py +1 -1
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/errors/resource.py +1 -1
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/errors/responses.py +32 -39
- fastapi_mongo_base-1.5.3/src/fastapi_mongo_base/errors/status.py +211 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/schemas.py +18 -0
- fastapi_mongo_base-1.5.3/src/fastapi_mongo_base/utils/usso/__init__.py +15 -0
- fastapi_mongo_base-1.5.3/src/fastapi_mongo_base/utils/usso/principals.py +60 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/utils/usso_routes.py +40 -42
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3/src/fastapi_mongo_base.egg-info}/PKG-INFO +1 -1
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base.egg-info/SOURCES.txt +15 -2
- fastapi_mongo_base-1.5.3/tests/helpers/__init__.py +1 -0
- fastapi_mongo_base-1.5.3/tests/helpers/usso_mock.py +117 -0
- fastapi_mongo_base-1.5.3/tests/test_basic_utils.py +208 -0
- fastapi_mongo_base-1.5.3/tests/test_bsontools.py +40 -0
- fastapi_mongo_base-1.5.3/tests/test_exception_shims.py +19 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/test_http_errors.py +23 -7
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/test_i18n.py +6 -8
- fastapi_mongo_base-1.5.3/tests/test_monitoring_middleware.py +306 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/test_openapi_errors.py +18 -0
- fastapi_mongo_base-1.5.3/tests/test_sql_models.py +878 -0
- fastapi_mongo_base-1.5.3/tests/test_tasks.py +825 -0
- fastapi_mongo_base-1.5.3/tests/test_timezone.py +31 -0
- fastapi_mongo_base-1.5.3/tests/test_usso_principals.py +55 -0
- fastapi_mongo_base-1.5.3/tests/test_usso_routes.py +422 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/test_usso_routes_workspace.py +34 -2
- fastapi_mongo_base-1.5.2/src/fastapi_mongo_base/errors/http.py +0 -109
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/LICENSE.txt +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/MANIFEST.in +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/README.md +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/docs/boilerplate.md +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/docs/changelog.md +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/docs/contributing.md +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/docs/endpoints.md +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/docs/extra.css +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/docs/index.md +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/docs/installation.md +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/docs/license.md +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/docs/quickstart.md +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/docs/settings.md +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/docs/tutorial.md +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/docs/usage.md +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/mkdocs.yml +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/setup.cfg +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/__init__.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/core/__init__.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/core/app_factory.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/core/config.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/core/db.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/core/exceptions.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/db/__init__.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/db/mongo.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/db/redis.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/db/sql.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/errors/base.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/errors/handlers.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/errors/redis.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/errors/sql.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/i18n/__init__.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/i18n/languages.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/i18n/messages.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/logging/__init__.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/logging/formatters.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/middlewares/__init__.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/middlewares/prometheus.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/middlewares/timer.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/models.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/monitoring/__init__.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/monitoring/middleware.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/monitoring/mongo.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/monitoring/sentry.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/py.typed +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/routes.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/sql/__init__.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/sql/models.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/sql/session.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/tasks.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/utils/__init__.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/utils/basic.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/utils/bsontools.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/utils/timezone.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base.egg-info/dependency_links.txt +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base.egg-info/requires.txt +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base.egg-info/top_level.txt +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/__init__.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/app/__init__.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/app/logs/app.log +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/app/main.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/app/server.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/conftest.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/test_apis.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/test_app.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/test_db.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/test_middlewares.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/test_mongodb_errors.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/test_monitoring_mongo.py +0 -0
- {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/test_sentry.py +0 -0
{fastapi_mongo_base-1.5.2/src/fastapi_mongo_base.egg-info → fastapi_mongo_base-1.5.3}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-mongo-base
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.3
|
|
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>
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fastapi-mongo-base"
|
|
7
|
-
version = "1.5.
|
|
7
|
+
version = "1.5.3"
|
|
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.10"
|
|
@@ -103,7 +103,7 @@ select = [
|
|
|
103
103
|
"ERA", # remove commented code
|
|
104
104
|
"RUF", # ruff-specific improvements
|
|
105
105
|
]
|
|
106
|
-
per-file-ignores = { "tests/*" = ["G004", "S101"], "server/db.py" = ["TRY"] }
|
|
106
|
+
per-file-ignores = { "tests/*" = ["G004", "S101", "ASYNC", "D", "ANN", "N806", "E501", "ERA001", "RUF029", "TRY300"], "server/db.py" = ["TRY"] }
|
|
107
107
|
ignore = ["TRY003", "COM812", "B008", "D202", "D203", "D209", "D212", "D413"]
|
|
108
108
|
|
|
109
109
|
[tool.ruff.format]
|
{fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/errors/__init__.py
RENAMED
|
@@ -9,18 +9,6 @@ from .handlers import (
|
|
|
9
9
|
pydantic_exception_handler,
|
|
10
10
|
request_validation_exception_handler,
|
|
11
11
|
)
|
|
12
|
-
from .http import (
|
|
13
|
-
AlreadyExistsError,
|
|
14
|
-
BadRequestError,
|
|
15
|
-
ConflictError,
|
|
16
|
-
ForbiddenError,
|
|
17
|
-
GoneError,
|
|
18
|
-
LockedError,
|
|
19
|
-
NotFoundError,
|
|
20
|
-
PaymentRequiredError,
|
|
21
|
-
ServerError,
|
|
22
|
-
UnauthorizedError,
|
|
23
|
-
)
|
|
24
12
|
from .mongodb import (
|
|
25
13
|
DocumentAlreadyExistsError,
|
|
26
14
|
DocumentDuplicateKeyError,
|
|
@@ -51,6 +39,25 @@ from .responses import (
|
|
|
51
39
|
ValidationReason,
|
|
52
40
|
setup_openapi_errors,
|
|
53
41
|
)
|
|
42
|
+
from .status import (
|
|
43
|
+
AlreadyExistsError,
|
|
44
|
+
BadRequestError,
|
|
45
|
+
ConflictError,
|
|
46
|
+
FeatureNotImplementedError,
|
|
47
|
+
ForbiddenError,
|
|
48
|
+
GatewayTimeoutError,
|
|
49
|
+
GoneError,
|
|
50
|
+
LockedError,
|
|
51
|
+
MethodNotAllowedError,
|
|
52
|
+
NotFoundError,
|
|
53
|
+
PaymentRequiredError,
|
|
54
|
+
PreconditionFailedError,
|
|
55
|
+
ServerError,
|
|
56
|
+
ServiceUnavailableError,
|
|
57
|
+
TeaPotError,
|
|
58
|
+
TooManyRequestsError,
|
|
59
|
+
UnauthorizedError,
|
|
60
|
+
)
|
|
54
61
|
|
|
55
62
|
__all__ = [
|
|
56
63
|
"COMMON_ERROR_RESPONSES",
|
|
@@ -63,10 +70,13 @@ __all__ = [
|
|
|
63
70
|
"DocumentAlreadyExistsError",
|
|
64
71
|
"DocumentDuplicateKeyError",
|
|
65
72
|
"DocumentNotFoundError",
|
|
73
|
+
"FeatureNotImplementedError",
|
|
66
74
|
"ForbiddenError",
|
|
75
|
+
"GatewayTimeoutError",
|
|
67
76
|
"GoneError",
|
|
68
77
|
"InternalErrorResponseModel",
|
|
69
78
|
"LockedError",
|
|
79
|
+
"MethodNotAllowedError",
|
|
70
80
|
"MongoDBConnectionError",
|
|
71
81
|
"MongoDBError",
|
|
72
82
|
"MongoDBTimeoutError",
|
|
@@ -74,6 +84,7 @@ __all__ = [
|
|
|
74
84
|
"MultipleDocumentsFoundError",
|
|
75
85
|
"NotFoundError",
|
|
76
86
|
"PaymentRequiredError",
|
|
87
|
+
"PreconditionFailedError",
|
|
77
88
|
"RedisConnectionError",
|
|
78
89
|
"ResourceAlreadyExistsError",
|
|
79
90
|
"ResourceConflictError",
|
|
@@ -83,6 +94,9 @@ __all__ = [
|
|
|
83
94
|
"ResourceNotFoundError",
|
|
84
95
|
"ResourcePaymentRequiredError",
|
|
85
96
|
"ServerError",
|
|
97
|
+
"ServiceUnavailableError",
|
|
98
|
+
"TeaPotError",
|
|
99
|
+
"TooManyRequestsError",
|
|
86
100
|
"UnauthorizedError",
|
|
87
101
|
"ValidationErrorResponseModel",
|
|
88
102
|
"ValidationReason",
|
{fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/errors/responses.py
RENAMED
|
@@ -4,6 +4,9 @@ import fastapi
|
|
|
4
4
|
from fastapi.openapi.utils import get_openapi
|
|
5
5
|
from pydantic import BaseModel, ConfigDict, Field
|
|
6
6
|
|
|
7
|
+
from . import status as _status_errors
|
|
8
|
+
from .base import BaseHTTPException
|
|
9
|
+
|
|
7
10
|
|
|
8
11
|
class APIErrorResponseModel(BaseModel):
|
|
9
12
|
"""Structured error response for BaseHTTPException handlers."""
|
|
@@ -43,47 +46,37 @@ class InternalErrorResponseModel(BaseModel):
|
|
|
43
46
|
error: str = "Exception"
|
|
44
47
|
|
|
45
48
|
|
|
49
|
+
def _status_error_descriptions() -> dict[int, str]:
|
|
50
|
+
"""Map HTTP status codes to OpenAPI descriptions from status exceptions."""
|
|
51
|
+
descriptions: dict[int, set[str]] = {}
|
|
52
|
+
for name in dir(_status_errors):
|
|
53
|
+
exc_type = getattr(_status_errors, name)
|
|
54
|
+
if not (
|
|
55
|
+
isinstance(exc_type, type)
|
|
56
|
+
and issubclass(exc_type, BaseHTTPException)
|
|
57
|
+
):
|
|
58
|
+
continue
|
|
59
|
+
code = exc_type.status_code
|
|
60
|
+
if not isinstance(code, int):
|
|
61
|
+
continue
|
|
62
|
+
label = exc_type.message_en or name
|
|
63
|
+
descriptions.setdefault(code, set()).add(label)
|
|
64
|
+
return {
|
|
65
|
+
code: " / ".join(sorted(labels))
|
|
66
|
+
for code, labels in descriptions.items()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
46
70
|
COMMON_ERROR_RESPONSES: dict[int, dict[str, object]] = {
|
|
47
|
-
|
|
48
|
-
"model": APIErrorResponseModel,
|
|
49
|
-
"description": "Bad request",
|
|
50
|
-
},
|
|
51
|
-
401: {
|
|
52
|
-
"model": APIErrorResponseModel,
|
|
53
|
-
"description": "Unauthorized",
|
|
54
|
-
},
|
|
55
|
-
402: {
|
|
56
|
-
"model": APIErrorResponseModel,
|
|
57
|
-
"description": "Payment required",
|
|
58
|
-
},
|
|
59
|
-
403: {
|
|
71
|
+
code: {
|
|
60
72
|
"model": APIErrorResponseModel,
|
|
61
|
-
"description":
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
"model": APIErrorResponseModel,
|
|
69
|
-
"description": "Conflict",
|
|
70
|
-
},
|
|
71
|
-
410: {
|
|
72
|
-
"model": APIErrorResponseModel,
|
|
73
|
-
"description": "Gone",
|
|
74
|
-
},
|
|
75
|
-
422: {
|
|
76
|
-
"model": ValidationErrorResponseModel,
|
|
77
|
-
"description": "Validation error",
|
|
78
|
-
},
|
|
79
|
-
423: {
|
|
80
|
-
"model": APIErrorResponseModel,
|
|
81
|
-
"description": "Locked",
|
|
82
|
-
},
|
|
83
|
-
500: {
|
|
84
|
-
"model": APIErrorResponseModel,
|
|
85
|
-
"description": "Internal server error",
|
|
86
|
-
},
|
|
73
|
+
"description": description,
|
|
74
|
+
}
|
|
75
|
+
for code, description in _status_error_descriptions().items()
|
|
76
|
+
}
|
|
77
|
+
COMMON_ERROR_RESPONSES[422] = {
|
|
78
|
+
"model": ValidationErrorResponseModel,
|
|
79
|
+
"description": "Validation error",
|
|
87
80
|
}
|
|
88
81
|
|
|
89
82
|
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""HTTP status-code API error exceptions."""
|
|
2
|
+
|
|
3
|
+
from .base import BaseHTTPException
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BadRequestError(BaseHTTPException):
|
|
7
|
+
"""Raised when a request is invalid."""
|
|
8
|
+
|
|
9
|
+
status_code = 400
|
|
10
|
+
error_code = "bad_request"
|
|
11
|
+
message_en = "Bad request"
|
|
12
|
+
message_fa = "درخواست نامعتبر"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from usso.exceptions import PermissionDenied as _USSOPermissionDenied
|
|
17
|
+
from usso.exceptions import USSOException as _USSOException
|
|
18
|
+
|
|
19
|
+
class UnauthorizedError(BaseHTTPException, _USSOException):
|
|
20
|
+
"""Raised when a request is unauthorized (when USSO is installed)."""
|
|
21
|
+
|
|
22
|
+
status_code = 401
|
|
23
|
+
error_code = "unauthorized"
|
|
24
|
+
message_en = "Unauthorized"
|
|
25
|
+
message_fa = "لطفا وارد حساب کاربری خود شوید."
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
error_code: str | None = None,
|
|
30
|
+
detail: str | None = None,
|
|
31
|
+
message: dict | None = None,
|
|
32
|
+
**kwargs: object,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Initialize with package HTTP error fields (USSO-compatible)."""
|
|
35
|
+
BaseHTTPException.__init__(
|
|
36
|
+
self,
|
|
37
|
+
status_code=self.status_code,
|
|
38
|
+
error_code=error_code or self.error_code,
|
|
39
|
+
detail=detail,
|
|
40
|
+
message=message,
|
|
41
|
+
**kwargs,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
class ForbiddenError(BaseHTTPException, _USSOPermissionDenied):
|
|
45
|
+
"""Raised when a request is forbidden (when USSO is installed)."""
|
|
46
|
+
|
|
47
|
+
status_code = 403
|
|
48
|
+
error_code = "permission_denied"
|
|
49
|
+
message_en = "Permission denied"
|
|
50
|
+
message_fa = "دسترسی غیر مجاز"
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
error_code: str | None = None,
|
|
55
|
+
detail: str | None = None,
|
|
56
|
+
message: dict | None = None,
|
|
57
|
+
**kwargs: object,
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Initialize with package HTTP error fields (USSO-compatible)."""
|
|
60
|
+
BaseHTTPException.__init__(
|
|
61
|
+
self,
|
|
62
|
+
status_code=self.status_code,
|
|
63
|
+
error_code=error_code or self.error_code,
|
|
64
|
+
detail=detail,
|
|
65
|
+
message=message,
|
|
66
|
+
**kwargs,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
except ImportError:
|
|
70
|
+
|
|
71
|
+
class UnauthorizedError(BaseHTTPException):
|
|
72
|
+
"""Raised when a request is unauthorized."""
|
|
73
|
+
|
|
74
|
+
status_code = 401
|
|
75
|
+
error_code = "unauthorized"
|
|
76
|
+
message_en = "Unauthorized"
|
|
77
|
+
message_fa = "لطفا وارد حساب کاربری خود شوید."
|
|
78
|
+
|
|
79
|
+
class ForbiddenError(BaseHTTPException):
|
|
80
|
+
"""Raised when a request is forbidden."""
|
|
81
|
+
|
|
82
|
+
status_code = 403
|
|
83
|
+
error_code = "permission_denied"
|
|
84
|
+
message_en = "Permission denied"
|
|
85
|
+
message_fa = "دسترسی غیر مجاز"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class PaymentRequiredError(BaseHTTPException):
|
|
89
|
+
"""Raised when payment is required before access."""
|
|
90
|
+
|
|
91
|
+
status_code = 402
|
|
92
|
+
error_code = "resource_payment_required"
|
|
93
|
+
message_en = "Resource payment required"
|
|
94
|
+
message_fa = "برای دسترسی، پرداخت لازم است"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class NotFoundError(BaseHTTPException):
|
|
98
|
+
"""Raised when a requested entity is not found."""
|
|
99
|
+
|
|
100
|
+
status_code = 404
|
|
101
|
+
error_code = "resource_not_found"
|
|
102
|
+
message_en = "Resource not found"
|
|
103
|
+
message_fa = "نمونه یافت نشد"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class MethodNotAllowedError(BaseHTTPException):
|
|
107
|
+
"""Raised when a request method is not allowed."""
|
|
108
|
+
|
|
109
|
+
status_code = 405
|
|
110
|
+
error_code = "method_not_allowed"
|
|
111
|
+
message_en = "Method not allowed"
|
|
112
|
+
message_fa = "متد درخواستی مجاز نیست"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class AlreadyExistsError(BaseHTTPException):
|
|
116
|
+
"""Raised when an entity already exists."""
|
|
117
|
+
|
|
118
|
+
status_code = 409
|
|
119
|
+
error_code = "resource_already_exists"
|
|
120
|
+
message_en = "Resource already exists"
|
|
121
|
+
message_fa = "نمونهی مشابه وجود دارد"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ConflictError(BaseHTTPException):
|
|
125
|
+
"""Raised when a request conflicts with current state."""
|
|
126
|
+
|
|
127
|
+
status_code = 409
|
|
128
|
+
error_code = "resource_conflict"
|
|
129
|
+
message_en = "Resource conflict"
|
|
130
|
+
message_fa = "اطلاعات ارسال شده تداخل دارد"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class GoneError(BaseHTTPException):
|
|
134
|
+
"""Raised when an entity is no longer available."""
|
|
135
|
+
|
|
136
|
+
status_code = 410
|
|
137
|
+
error_code = "resource_gone"
|
|
138
|
+
message_en = "Resource gone"
|
|
139
|
+
message_fa = "در دسترس نیست"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class PreconditionFailedError(BaseHTTPException):
|
|
143
|
+
"""Raised when a precondition failed."""
|
|
144
|
+
|
|
145
|
+
status_code = 412
|
|
146
|
+
error_code = "precondition_failed"
|
|
147
|
+
message_en = "Precondition failed"
|
|
148
|
+
message_fa = "پیششرط نقض شده است"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class TeaPotError(BaseHTTPException):
|
|
152
|
+
"""Raised when a request is a teapot."""
|
|
153
|
+
|
|
154
|
+
status_code = 418
|
|
155
|
+
error_code = "teapot"
|
|
156
|
+
message_en = "I'm a teapot"
|
|
157
|
+
message_fa = "من نمیخواهم پاسخ بدهم!"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class LockedError(BaseHTTPException):
|
|
161
|
+
"""Raised when an entity is locked."""
|
|
162
|
+
|
|
163
|
+
status_code = 423
|
|
164
|
+
error_code = "resource_locked"
|
|
165
|
+
message_en = "Resource locked"
|
|
166
|
+
message_fa = "قفل شده است"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class TooManyRequestsError(BaseHTTPException):
|
|
170
|
+
"""Raised when a request is made too many times."""
|
|
171
|
+
|
|
172
|
+
status_code = 429
|
|
173
|
+
error_code = "too_many_requests"
|
|
174
|
+
message_en = "Too many requests"
|
|
175
|
+
message_fa = "درخواستهای همزمان زیادی ارسال شده است."
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class ServerError(BaseHTTPException):
|
|
179
|
+
"""Base exception for application API errors."""
|
|
180
|
+
|
|
181
|
+
status_code: int = 500
|
|
182
|
+
error_code: str = "internal_server_error"
|
|
183
|
+
message_en: str = "Internal server error"
|
|
184
|
+
message_fa: str | None = "خطای داخلی سرور رخ داده است"
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class FeatureNotImplementedError(BaseHTTPException):
|
|
188
|
+
"""Raised when a requested feature is not implemented (HTTP 501)."""
|
|
189
|
+
|
|
190
|
+
status_code = 501
|
|
191
|
+
error_code = "not_implemented"
|
|
192
|
+
message_en = "Not implemented"
|
|
193
|
+
message_fa = "این قابلیت هنوز فعال نشده است."
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class ServiceUnavailableError(BaseHTTPException):
|
|
197
|
+
"""Raised when a service is unavailable."""
|
|
198
|
+
|
|
199
|
+
status_code = 503
|
|
200
|
+
error_code = "service_unavailable"
|
|
201
|
+
message_en = "Service unavailable"
|
|
202
|
+
message_fa = "سرویس در دسترس نیست."
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class GatewayTimeoutError(BaseHTTPException):
|
|
206
|
+
"""Raised when a request times out."""
|
|
207
|
+
|
|
208
|
+
status_code = 504
|
|
209
|
+
error_code = "gateway_timeout"
|
|
210
|
+
message_en = "Gateway timeout"
|
|
211
|
+
message_fa = "درگاه در دسترس نیست."
|
|
@@ -103,6 +103,24 @@ class BaseEntitySchema(BaseModel):
|
|
|
103
103
|
])
|
|
104
104
|
|
|
105
105
|
|
|
106
|
+
class OwnerOverrideCreateMixin(BaseModel):
|
|
107
|
+
"""Optional user owner override on create payloads (service/admin)."""
|
|
108
|
+
|
|
109
|
+
user_id: str | None = Field(
|
|
110
|
+
None,
|
|
111
|
+
description="Target user id (omitted = authenticated user)",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class OwnedOverrideCreateMixin(BaseModel):
|
|
116
|
+
"""Optional owner override on create payloads (service/admin)."""
|
|
117
|
+
|
|
118
|
+
owner_id: str | None = Field(
|
|
119
|
+
None,
|
|
120
|
+
description="Target owner id (omitted = authenticated owner)",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
106
124
|
class UserOwnedEntitySchema(BaseEntitySchema):
|
|
107
125
|
"""Schema for entities owned by a user."""
|
|
108
126
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""USSO integration helpers (optional ``[usso]`` extra)."""
|
|
2
|
+
|
|
3
|
+
from .principals import (
|
|
4
|
+
SERVICE_SUB_TYPES,
|
|
5
|
+
is_service_auth,
|
|
6
|
+
is_service_request,
|
|
7
|
+
is_service_user,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"SERVICE_SUB_TYPES",
|
|
12
|
+
"is_service_auth",
|
|
13
|
+
"is_service_request",
|
|
14
|
+
"is_service_user",
|
|
15
|
+
]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Detect USSO service principals (API key, agent) on requests and claims."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import Protocol, runtime_checkable
|
|
5
|
+
|
|
6
|
+
SERVICE_SUB_TYPES = frozenset({"agent", "api_key"})
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@runtime_checkable
|
|
10
|
+
class RequestWithHeaders(Protocol):
|
|
11
|
+
"""Minimal request interface needed to identify API-key requests."""
|
|
12
|
+
|
|
13
|
+
headers: Mapping[str, str]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@runtime_checkable
|
|
17
|
+
class UserWithClaims(Protocol):
|
|
18
|
+
"""Minimal user interface for USSO-style service subject types."""
|
|
19
|
+
|
|
20
|
+
claims: Mapping[str, object] | None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _header_api_key(headers: Mapping[str, str]) -> str | None:
|
|
24
|
+
for name in ("x-api-key", "X-Api-Key", "X-API-Key", "X_API_KEY"):
|
|
25
|
+
value = headers.get(name)
|
|
26
|
+
if value:
|
|
27
|
+
return str(value)
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def is_service_request(request: object) -> bool:
|
|
32
|
+
"""Return True when the request authenticated via an API key header."""
|
|
33
|
+
if not isinstance(request, RequestWithHeaders):
|
|
34
|
+
msg = "request must expose headers mapping"
|
|
35
|
+
raise TypeError(msg)
|
|
36
|
+
return _header_api_key(request.headers) is not None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def is_service_user(user: object) -> bool:
|
|
40
|
+
"""
|
|
41
|
+
Return True when ``user`` is a service principal (agent or API key).
|
|
42
|
+
|
|
43
|
+
Relies on USSO ``sub_type`` claim values ``agent`` and ``api_key``.
|
|
44
|
+
"""
|
|
45
|
+
claims = getattr(user, "claims", None)
|
|
46
|
+
if not claims:
|
|
47
|
+
return False
|
|
48
|
+
sub_type = claims.get("sub_type")
|
|
49
|
+
return sub_type in SERVICE_SUB_TYPES
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def is_service_auth(
|
|
53
|
+
*,
|
|
54
|
+
request: object | None = None,
|
|
55
|
+
user: object | None = None,
|
|
56
|
+
) -> bool:
|
|
57
|
+
"""Return True when either the request or user indicates service auth."""
|
|
58
|
+
if request is not None and is_service_request(request):
|
|
59
|
+
return True
|
|
60
|
+
return bool(user is not None and is_service_user(user))
|