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.
Files changed (101) hide show
  1. {fastapi_mongo_base-1.5.2/src/fastapi_mongo_base.egg-info → fastapi_mongo_base-1.5.3}/PKG-INFO +1 -1
  2. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/pyproject.toml +2 -2
  3. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/pytest.ini +1 -1
  4. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/errors/__init__.py +26 -12
  5. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/errors/exceptions.py +1 -1
  6. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/errors/mongodb.py +1 -1
  7. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/errors/resource.py +1 -1
  8. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/errors/responses.py +32 -39
  9. fastapi_mongo_base-1.5.3/src/fastapi_mongo_base/errors/status.py +211 -0
  10. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/schemas.py +18 -0
  11. fastapi_mongo_base-1.5.3/src/fastapi_mongo_base/utils/usso/__init__.py +15 -0
  12. fastapi_mongo_base-1.5.3/src/fastapi_mongo_base/utils/usso/principals.py +60 -0
  13. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/utils/usso_routes.py +40 -42
  14. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3/src/fastapi_mongo_base.egg-info}/PKG-INFO +1 -1
  15. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base.egg-info/SOURCES.txt +15 -2
  16. fastapi_mongo_base-1.5.3/tests/helpers/__init__.py +1 -0
  17. fastapi_mongo_base-1.5.3/tests/helpers/usso_mock.py +117 -0
  18. fastapi_mongo_base-1.5.3/tests/test_basic_utils.py +208 -0
  19. fastapi_mongo_base-1.5.3/tests/test_bsontools.py +40 -0
  20. fastapi_mongo_base-1.5.3/tests/test_exception_shims.py +19 -0
  21. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/test_http_errors.py +23 -7
  22. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/test_i18n.py +6 -8
  23. fastapi_mongo_base-1.5.3/tests/test_monitoring_middleware.py +306 -0
  24. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/test_openapi_errors.py +18 -0
  25. fastapi_mongo_base-1.5.3/tests/test_sql_models.py +878 -0
  26. fastapi_mongo_base-1.5.3/tests/test_tasks.py +825 -0
  27. fastapi_mongo_base-1.5.3/tests/test_timezone.py +31 -0
  28. fastapi_mongo_base-1.5.3/tests/test_usso_principals.py +55 -0
  29. fastapi_mongo_base-1.5.3/tests/test_usso_routes.py +422 -0
  30. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/test_usso_routes_workspace.py +34 -2
  31. fastapi_mongo_base-1.5.2/src/fastapi_mongo_base/errors/http.py +0 -109
  32. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/LICENSE.txt +0 -0
  33. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/MANIFEST.in +0 -0
  34. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/README.md +0 -0
  35. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/docs/boilerplate.md +0 -0
  36. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/docs/changelog.md +0 -0
  37. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/docs/contributing.md +0 -0
  38. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/docs/endpoints.md +0 -0
  39. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/docs/extra.css +0 -0
  40. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/docs/index.md +0 -0
  41. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/docs/installation.md +0 -0
  42. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/docs/license.md +0 -0
  43. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/docs/quickstart.md +0 -0
  44. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/docs/settings.md +0 -0
  45. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/docs/tutorial.md +0 -0
  46. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/docs/usage.md +0 -0
  47. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/mkdocs.yml +0 -0
  48. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/setup.cfg +0 -0
  49. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/__init__.py +0 -0
  50. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/core/__init__.py +0 -0
  51. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/core/app_factory.py +0 -0
  52. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/core/config.py +0 -0
  53. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/core/db.py +0 -0
  54. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/core/exceptions.py +0 -0
  55. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/db/__init__.py +0 -0
  56. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/db/mongo.py +0 -0
  57. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/db/redis.py +0 -0
  58. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/db/sql.py +0 -0
  59. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/errors/base.py +0 -0
  60. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/errors/handlers.py +0 -0
  61. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/errors/redis.py +0 -0
  62. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/errors/sql.py +0 -0
  63. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/i18n/__init__.py +0 -0
  64. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/i18n/languages.py +0 -0
  65. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/i18n/messages.py +0 -0
  66. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/logging/__init__.py +0 -0
  67. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/logging/formatters.py +0 -0
  68. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/middlewares/__init__.py +0 -0
  69. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/middlewares/prometheus.py +0 -0
  70. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/middlewares/timer.py +0 -0
  71. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/models.py +0 -0
  72. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/monitoring/__init__.py +0 -0
  73. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/monitoring/middleware.py +0 -0
  74. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/monitoring/mongo.py +0 -0
  75. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/monitoring/sentry.py +0 -0
  76. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/py.typed +0 -0
  77. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/routes.py +0 -0
  78. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/sql/__init__.py +0 -0
  79. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/sql/models.py +0 -0
  80. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/sql/session.py +0 -0
  81. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/tasks.py +0 -0
  82. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/utils/__init__.py +0 -0
  83. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/utils/basic.py +0 -0
  84. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/utils/bsontools.py +0 -0
  85. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base/utils/timezone.py +0 -0
  86. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base.egg-info/dependency_links.txt +0 -0
  87. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base.egg-info/requires.txt +0 -0
  88. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/src/fastapi_mongo_base.egg-info/top_level.txt +0 -0
  89. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/__init__.py +0 -0
  90. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/app/__init__.py +0 -0
  91. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/app/logs/app.log +0 -0
  92. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/app/main.py +0 -0
  93. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/app/server.py +0 -0
  94. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/conftest.py +0 -0
  95. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/test_apis.py +0 -0
  96. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/test_app.py +0 -0
  97. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/test_db.py +0 -0
  98. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/test_middlewares.py +0 -0
  99. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/test_mongodb_errors.py +0 -0
  100. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/test_monitoring_mongo.py +0 -0
  101. {fastapi_mongo_base-1.5.2 → fastapi_mongo_base-1.5.3}/tests/test_sentry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-mongo-base
3
- Version: 1.5.2
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.2"
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]
@@ -11,7 +11,7 @@ addopts =
11
11
  --cov=src
12
12
  --cov-report=term-missing
13
13
  --cov-report=html
14
- --cov-fail-under=50
14
+ --cov-fail-under=85
15
15
 
16
16
  filterwarnings =
17
17
  ignore:.*model_fields.*:DeprecationWarning
@@ -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",
@@ -1,7 +1,7 @@
1
1
  """Exceptions for the fastapi-mongo-base package."""
2
2
 
3
3
  from .base import BaseHTTPException
4
- from .http import (
4
+ from .status import (
5
5
  AlreadyExistsError,
6
6
  BadRequestError,
7
7
  ConflictError,
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  from typing import TYPE_CHECKING
6
6
 
7
7
  from .base import BaseHTTPException
8
- from .http import (
8
+ from .status import (
9
9
  AlreadyExistsError,
10
10
  NotFoundError,
11
11
  )
@@ -1,6 +1,6 @@
1
1
  """Backward compatibility aliases for HTTP client errors."""
2
2
 
3
- from .http import (
3
+ from .status import (
4
4
  AlreadyExistsError,
5
5
  ConflictError,
6
6
  ForbiddenError,
@@ -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
- 400: {
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": "Forbidden",
62
- },
63
- 404: {
64
- "model": APIErrorResponseModel,
65
- "description": "Not found",
66
- },
67
- 409: {
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))