10xgraph-api 0.7.0__py3-none-any.whl

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 (203) hide show
  1. 10xgraph_api-0.7.0.dist-info/METADATA +618 -0
  2. 10xgraph_api-0.7.0.dist-info/RECORD +203 -0
  3. 10xgraph_api-0.7.0.dist-info/WHEEL +5 -0
  4. 10xgraph_api-0.7.0.dist-info/entry_points.txt +3 -0
  5. 10xgraph_api-0.7.0.dist-info/licenses/LICENSE +21 -0
  6. 10xgraph_api-0.7.0.dist-info/top_level.txt +2 -0
  7. agentflow_cli/__init__.py +69 -0
  8. tenxgraph_api/__init__.py +23 -0
  9. tenxgraph_api/cli/__init__.py +6 -0
  10. tenxgraph_api/cli/capabilities.py +127 -0
  11. tenxgraph_api/cli/commands/__init__.py +49 -0
  12. tenxgraph_api/cli/commands/api.py +262 -0
  13. tenxgraph_api/cli/commands/audit.py +213 -0
  14. tenxgraph_api/cli/commands/build.py +305 -0
  15. tenxgraph_api/cli/commands/config.py +71 -0
  16. tenxgraph_api/cli/commands/demo.py +93 -0
  17. tenxgraph_api/cli/commands/eval.py +909 -0
  18. tenxgraph_api/cli/commands/init.py +531 -0
  19. tenxgraph_api/cli/commands/skills.py +532 -0
  20. tenxgraph_api/cli/commands/test.py +113 -0
  21. tenxgraph_api/cli/commands/version.py +47 -0
  22. tenxgraph_api/cli/config_editor/__init__.py +15 -0
  23. tenxgraph_api/cli/config_editor/schema.py +566 -0
  24. tenxgraph_api/cli/config_editor/server.py +240 -0
  25. tenxgraph_api/cli/config_editor/static/app.css +2 -0
  26. tenxgraph_api/cli/config_editor/static/app.js +3 -0
  27. tenxgraph_api/cli/config_editor/static/favicon.svg +11 -0
  28. tenxgraph_api/cli/config_editor/static/index.html +20 -0
  29. tenxgraph_api/cli/config_editor/store.py +120 -0
  30. tenxgraph_api/cli/config_editor/validation.py +377 -0
  31. tenxgraph_api/cli/constants.py +112 -0
  32. tenxgraph_api/cli/context.py +46 -0
  33. tenxgraph_api/cli/core/__init__.py +0 -0
  34. tenxgraph_api/cli/core/animations.py +550 -0
  35. tenxgraph_api/cli/core/config.py +279 -0
  36. tenxgraph_api/cli/core/output.py +625 -0
  37. tenxgraph_api/cli/core/prompts.py +220 -0
  38. tenxgraph_api/cli/core/screen.py +287 -0
  39. tenxgraph_api/cli/core/steps.py +621 -0
  40. tenxgraph_api/cli/core/theme.py +242 -0
  41. tenxgraph_api/cli/core/validation.py +251 -0
  42. tenxgraph_api/cli/exceptions.py +129 -0
  43. tenxgraph_api/cli/logger.py +84 -0
  44. tenxgraph_api/cli/main.py +936 -0
  45. tenxgraph_api/cli/templates/__init__.py +0 -0
  46. tenxgraph_api/cli/templates/defaults.py +844 -0
  47. tenxgraph_api/cli/templates/dev/.env.example +1 -0
  48. tenxgraph_api/cli/templates/dev/10xgraph.json +16 -0
  49. tenxgraph_api/cli/templates/dev/graph/__init__.py +0 -0
  50. tenxgraph_api/cli/templates/dev/graph/agent.py +63 -0
  51. tenxgraph_api/cli/templates/prod/.env.example +75 -0
  52. tenxgraph_api/cli/templates/prod/.pre-commit-config.yaml +50 -0
  53. tenxgraph_api/cli/templates/prod/.python-version +1 -0
  54. tenxgraph_api/cli/templates/prod/10xgraph.json +42 -0
  55. tenxgraph_api/cli/templates/prod/auth/__init__.py +0 -0
  56. tenxgraph_api/cli/templates/prod/auth/agent_auth.py +46 -0
  57. tenxgraph_api/cli/templates/prod/evals/__init__.py +0 -0
  58. tenxgraph_api/cli/templates/prod/evals/user_simulator_eval.py +76 -0
  59. tenxgraph_api/cli/templates/prod/evals/weather_agents_eval.py +53 -0
  60. tenxgraph_api/cli/templates/prod/graph/__init__.py +0 -0
  61. tenxgraph_api/cli/templates/prod/graph/agent.py +50 -0
  62. tenxgraph_api/cli/templates/prod/graph/state.py +5 -0
  63. tenxgraph_api/cli/templates/prod/graph/thread_name_generator.py +7 -0
  64. tenxgraph_api/cli/templates/prod/graph/tools/__init__.py +0 -0
  65. tenxgraph_api/cli/templates/prod/graph/tools/weather_tool.py +43 -0
  66. tenxgraph_api/cli/templates/prod/graph/validators/__init__.py +0 -0
  67. tenxgraph_api/cli/templates/prod/graph/validators/lifecyle.py +107 -0
  68. tenxgraph_api/cli/templates/prod/graph/validators/manager.py +9 -0
  69. tenxgraph_api/cli/templates/prod/graph/validators/validators.py +21 -0
  70. tenxgraph_api/cli/templates/prod/pyproject.toml +79 -0
  71. tenxgraph_api/cli/templates/prod/tests/__init__.py +0 -0
  72. tenxgraph_api/cli/templates/prod/tests/conftest.py +6 -0
  73. tenxgraph_api/cli/templates/prod/tests/test_agent_eval.py +78 -0
  74. tenxgraph_api/cli/templates/prod/tests/test_catalog_tools.py +47 -0
  75. tenxgraph_api/cli/templates/prod/tests/test_graph_nodes.py +57 -0
  76. tenxgraph_api/cli/templates/skills/10xgraph/SKILL.md +92 -0
  77. tenxgraph_api/cli/templates/skills/10xgraph/references/agents-and-tools.md +389 -0
  78. tenxgraph_api/cli/templates/skills/10xgraph/references/api-client.md +61 -0
  79. tenxgraph_api/cli/templates/skills/10xgraph/references/api-configuration.md +75 -0
  80. tenxgraph_api/cli/templates/skills/10xgraph/references/api-settings-and-middleware.md +89 -0
  81. tenxgraph_api/cli/templates/skills/10xgraph/references/architecture.md +91 -0
  82. tenxgraph_api/cli/templates/skills/10xgraph/references/auth-and-authorization.md +149 -0
  83. tenxgraph_api/cli/templates/skills/10xgraph/references/callbacks-and-command.md +283 -0
  84. tenxgraph_api/cli/templates/skills/10xgraph/references/checkpointing-and-threads.md +63 -0
  85. tenxgraph_api/cli/templates/skills/10xgraph/references/cli-commands.md +73 -0
  86. tenxgraph_api/cli/templates/skills/10xgraph/references/client-auth-and-errors.md +94 -0
  87. tenxgraph_api/cli/templates/skills/10xgraph/references/client-messages-invoke-stream.md +115 -0
  88. tenxgraph_api/cli/templates/skills/10xgraph/references/client-threads-memory-files.md +80 -0
  89. tenxgraph_api/cli/templates/skills/10xgraph/references/context-id-background.md +80 -0
  90. tenxgraph_api/cli/templates/skills/10xgraph/references/dependency-injection.md +58 -0
  91. tenxgraph_api/cli/templates/skills/10xgraph/references/evaluation.md +500 -0
  92. tenxgraph_api/cli/templates/skills/10xgraph/references/id-and-thread-name-generators.md +69 -0
  93. tenxgraph_api/cli/templates/skills/10xgraph/references/media-and-files.md +66 -0
  94. tenxgraph_api/cli/templates/skills/10xgraph/references/memory-and-store.md +70 -0
  95. tenxgraph_api/cli/templates/skills/10xgraph/references/prebuilt-agents-and-tools.md +394 -0
  96. tenxgraph_api/cli/templates/skills/10xgraph/references/production-runtime.md +56 -0
  97. tenxgraph_api/cli/templates/skills/10xgraph/references/providers-and-adapters.md +89 -0
  98. tenxgraph_api/cli/templates/skills/10xgraph/references/publishers-and-runtime-protocols.md +84 -0
  99. tenxgraph_api/cli/templates/skills/10xgraph/references/rate-limiting.md +194 -0
  100. tenxgraph_api/cli/templates/skills/10xgraph/references/realtime.md +172 -0
  101. tenxgraph_api/cli/templates/skills/10xgraph/references/remote-tools.md +55 -0
  102. tenxgraph_api/cli/templates/skills/10xgraph/references/rest-api-and-errors.md +97 -0
  103. tenxgraph_api/cli/templates/skills/10xgraph/references/security-and-validators.md +86 -0
  104. tenxgraph_api/cli/templates/skills/10xgraph/references/state-and-messages.md +68 -0
  105. tenxgraph_api/cli/templates/skills/10xgraph/references/state-graph.md +232 -0
  106. tenxgraph_api/cli/templates/skills/10xgraph/references/stream-emitter.md +144 -0
  107. tenxgraph_api/cli/templates/skills/10xgraph/references/streaming.md +53 -0
  108. tenxgraph_api/cli/templates/skills/10xgraph/references/testing-and-evaluation.md +64 -0
  109. tenxgraph_api/cli/templates/skills/10xgraph/references/unit-testing.md +202 -0
  110. tenxgraph_api/cli/templates/skills/copilot/10xgraph.instructions.md +108 -0
  111. tenxgraph_api/py.typed +0 -0
  112. tenxgraph_api/src/__init__.py +0 -0
  113. tenxgraph_api/src/app/__init__.py +13 -0
  114. tenxgraph_api/src/app/core/__init__.py +15 -0
  115. tenxgraph_api/src/app/core/auth/__init__.py +0 -0
  116. tenxgraph_api/src/app/core/auth/auth_backend.py +38 -0
  117. tenxgraph_api/src/app/core/auth/authorization.py +379 -0
  118. tenxgraph_api/src/app/core/auth/base_auth.py +27 -0
  119. tenxgraph_api/src/app/core/auth/jwt_auth.py +122 -0
  120. tenxgraph_api/src/app/core/auth/ownership_resolver.py +170 -0
  121. tenxgraph_api/src/app/core/auth/permissions.py +354 -0
  122. tenxgraph_api/src/app/core/auth/request_config.py +130 -0
  123. tenxgraph_api/src/app/core/auth/route_guard.py +105 -0
  124. tenxgraph_api/src/app/core/config/__init__.py +0 -0
  125. tenxgraph_api/src/app/core/config/graph_config.py +532 -0
  126. tenxgraph_api/src/app/core/config/media_settings.py +72 -0
  127. tenxgraph_api/src/app/core/config/sentry_config.py +60 -0
  128. tenxgraph_api/src/app/core/config/settings.py +202 -0
  129. tenxgraph_api/src/app/core/config/setup_logs.py +65 -0
  130. tenxgraph_api/src/app/core/config/setup_middleware.py +483 -0
  131. tenxgraph_api/src/app/core/config/worker_middleware.py +99 -0
  132. tenxgraph_api/src/app/core/exceptions/__init__.py +17 -0
  133. tenxgraph_api/src/app/core/exceptions/general_exception.py +58 -0
  134. tenxgraph_api/src/app/core/exceptions/handle_errors.py +363 -0
  135. tenxgraph_api/src/app/core/exceptions/resources_exceptions.py +72 -0
  136. tenxgraph_api/src/app/core/exceptions/user_exception.py +58 -0
  137. tenxgraph_api/src/app/core/middleware/__init__.py +25 -0
  138. tenxgraph_api/src/app/core/middleware/rate_limit/__init__.py +15 -0
  139. tenxgraph_api/src/app/core/middleware/rate_limit/base.py +27 -0
  140. tenxgraph_api/src/app/core/middleware/rate_limit/factory.py +98 -0
  141. tenxgraph_api/src/app/core/middleware/rate_limit/keying.py +133 -0
  142. tenxgraph_api/src/app/core/middleware/rate_limit/memory.py +98 -0
  143. tenxgraph_api/src/app/core/middleware/rate_limit/middleware.py +90 -0
  144. tenxgraph_api/src/app/core/middleware/rate_limit/redis.py +115 -0
  145. tenxgraph_api/src/app/core/middleware/request_limits.py +154 -0
  146. tenxgraph_api/src/app/core/middleware/security_headers.py +239 -0
  147. tenxgraph_api/src/app/core/utils/__init__.py +14 -0
  148. tenxgraph_api/src/app/core/utils/log_sanitizer.py +216 -0
  149. tenxgraph_api/src/app/loader.py +516 -0
  150. tenxgraph_api/src/app/main.py +180 -0
  151. tenxgraph_api/src/app/routers/__init__.py +4 -0
  152. tenxgraph_api/src/app/routers/ag_ui/__init__.py +10 -0
  153. tenxgraph_api/src/app/routers/ag_ui/converter.py +195 -0
  154. tenxgraph_api/src/app/routers/ag_ui/event_mapper.py +353 -0
  155. tenxgraph_api/src/app/routers/ag_ui/router.py +90 -0
  156. tenxgraph_api/src/app/routers/ag_ui/service.py +193 -0
  157. tenxgraph_api/src/app/routers/checkpointer/__init__.py +1 -0
  158. tenxgraph_api/src/app/routers/checkpointer/router.py +482 -0
  159. tenxgraph_api/src/app/routers/checkpointer/schemas/__init__.py +1 -0
  160. tenxgraph_api/src/app/routers/checkpointer/schemas/checkpointer_schemas.py +121 -0
  161. tenxgraph_api/src/app/routers/checkpointer/services/__init__.py +1 -0
  162. tenxgraph_api/src/app/routers/checkpointer/services/checkpointer_service.py +300 -0
  163. tenxgraph_api/src/app/routers/evals/__init__.py +6 -0
  164. tenxgraph_api/src/app/routers/evals/router.py +38 -0
  165. tenxgraph_api/src/app/routers/evals/services/__init__.py +1 -0
  166. tenxgraph_api/src/app/routers/evals/services/eval_report_service.py +354 -0
  167. tenxgraph_api/src/app/routers/graph/__init__.py +6 -0
  168. tenxgraph_api/src/app/routers/graph/realtime_guard.py +161 -0
  169. tenxgraph_api/src/app/routers/graph/router.py +821 -0
  170. tenxgraph_api/src/app/routers/graph/schemas/__init__.py +1 -0
  171. tenxgraph_api/src/app/routers/graph/schemas/graph_schemas.py +386 -0
  172. tenxgraph_api/src/app/routers/graph/services/__init__.py +1 -0
  173. tenxgraph_api/src/app/routers/graph/services/graph_service.py +1219 -0
  174. tenxgraph_api/src/app/routers/graph/services/multimodal_preprocessor.py +164 -0
  175. tenxgraph_api/src/app/routers/media/__init__.py +506 -0
  176. tenxgraph_api/src/app/routers/media/router.py +242 -0
  177. tenxgraph_api/src/app/routers/media/schemas.py +39 -0
  178. tenxgraph_api/src/app/routers/ping/__init__.py +4 -0
  179. tenxgraph_api/src/app/routers/ping/router.py +28 -0
  180. tenxgraph_api/src/app/routers/setup_router.py +60 -0
  181. tenxgraph_api/src/app/routers/store/__init__.py +4 -0
  182. tenxgraph_api/src/app/routers/store/router.py +200 -0
  183. tenxgraph_api/src/app/routers/store/schemas/__init__.py +28 -0
  184. tenxgraph_api/src/app/routers/store/schemas/store_schemas.py +200 -0
  185. tenxgraph_api/src/app/routers/store/services/__init__.py +4 -0
  186. tenxgraph_api/src/app/routers/store/services/store_service.py +203 -0
  187. tenxgraph_api/src/app/tasks/__init__.py +0 -0
  188. tenxgraph_api/src/app/tasks/user_tasks.py +38 -0
  189. tenxgraph_api/src/app/utils/__init__.py +12 -0
  190. tenxgraph_api/src/app/utils/callable_helper.py +26 -0
  191. tenxgraph_api/src/app/utils/media/__init__.py +15 -0
  192. tenxgraph_api/src/app/utils/media/extractor.py +172 -0
  193. tenxgraph_api/src/app/utils/media/pipeline.py +83 -0
  194. tenxgraph_api/src/app/utils/parse_output.py +17 -0
  195. tenxgraph_api/src/app/utils/response_helper.py +129 -0
  196. tenxgraph_api/src/app/utils/schemas/__init__.py +11 -0
  197. tenxgraph_api/src/app/utils/schemas/output_schemas.py +28 -0
  198. tenxgraph_api/src/app/utils/schemas/user_schemas.py +13 -0
  199. tenxgraph_api/src/app/utils/snowflake_id_generator.py +90 -0
  200. tenxgraph_api/src/app/utils/swagger_helper.py +160 -0
  201. tenxgraph_api/src/app/utils/telemetry_store.py +123 -0
  202. tenxgraph_api/src/app/utils/thread_name_generator.py +304 -0
  203. tenxgraph_api/src/app/worker.py +91 -0
@@ -0,0 +1,363 @@
1
+ # Handle all exceptions of the 10xGraph API here
2
+ from fastapi import FastAPI
3
+ from fastapi.exceptions import RequestValidationError
4
+ from starlette.exceptions import HTTPException
5
+ from starlette.requests import Request
6
+ from tenxgraph.core.exceptions import (
7
+ GraphError,
8
+ GraphRecursionError,
9
+ MetricsError,
10
+ NodeError,
11
+ SchemaVersionError,
12
+ SerializationError,
13
+ StorageError,
14
+ TransientStorageError,
15
+ )
16
+ from tenxgraph.utils.validators import ValidationError
17
+
18
+ from tenxgraph_api.src.app.core import logger
19
+ from tenxgraph_api.src.app.core.config.settings import get_settings
20
+ from tenxgraph_api.src.app.utils import error_response
21
+ from tenxgraph_api.src.app.utils.schemas import ErrorSchemas
22
+
23
+ from .resources_exceptions import ResourceNotFoundError as APIResourceNotFoundError
24
+ from .user_exception import (
25
+ UserAccountError,
26
+ UserPermissionError,
27
+ )
28
+
29
+
30
+ def _sanitize_error_message(message: str, error_code: str, is_production: bool) -> str:
31
+ """
32
+ Sanitize error messages for production to avoid exposing internal details.
33
+
34
+ Args:
35
+ message: Original error message
36
+ error_code: Error code for the exception
37
+ is_production: Whether the app is in production mode
38
+
39
+ Returns:
40
+ Sanitized message (generic in production, detailed in development)
41
+ """
42
+ if not is_production:
43
+ return message
44
+
45
+ # Generic messages by status code category
46
+ generic_messages = {
47
+ "VALIDATION_ERROR": "The request data is invalid. Please check your input.",
48
+ "HTTPException": "An error occurred processing your request.",
49
+ "GRAPH_": "An error occurred executing the graph.",
50
+ "NODE_": "An error occurred in a graph node.",
51
+ "STORAGE_": "An error occurred accessing storage.",
52
+ "METRICS_": "An error occurred collecting metrics.",
53
+ "SCHEMA_VERSION_": "Schema version mismatch.",
54
+ "SERIALIZATION_": "An error occurred processing data.",
55
+ }
56
+
57
+ # Return generic message based on error code prefix
58
+ for prefix, generic_msg in generic_messages.items():
59
+ if error_code.startswith(prefix):
60
+ return generic_msg
61
+
62
+ return "An unexpected error occurred. Please contact support."
63
+
64
+
65
+ def init_errors_handler(app: FastAPI): # noqa: PLR0915
66
+ """
67
+ Initialize error handlers for the FastAPI application.
68
+
69
+ Args:
70
+ app (FastAPI): The FastAPI application instance.
71
+
72
+ Raises:
73
+ HTTPException: Handles HTTP exceptions.
74
+ RequestValidationError: Handles request validation errors.
75
+ ValueError: Handles value errors.
76
+ UserAccountError: Handles custom user account errors.
77
+ UserPermissionError: Handles custom user permission errors.
78
+ APIResourceNotFoundError: Handles custom API resource not found errors.
79
+ """
80
+ settings = get_settings()
81
+ is_production = settings.MODE == "production"
82
+
83
+ @app.exception_handler(HTTPException)
84
+ async def http_exception_handler(request: Request, exc: HTTPException):
85
+ logger.error(f"HTTP exception: url: {request.base_url}", exc_info=exc)
86
+
87
+ # Get request ID for tracking
88
+ request_id = getattr(request.state, "request_id", "unknown")
89
+
90
+ message = _sanitize_error_message(str(exc.detail), "HTTPException", is_production)
91
+
92
+ # Log full details but return sanitized message in production
93
+ if is_production:
94
+ logger.error(f"Request {request_id} - HTTPException details: {exc.detail}")
95
+
96
+ return error_response(
97
+ request,
98
+ error_code="HTTPException",
99
+ message=message,
100
+ status_code=exc.status_code,
101
+ )
102
+
103
+ @app.exception_handler(RequestValidationError)
104
+ async def validation_exception_handler(request: Request, exc: RequestValidationError):
105
+ request_id = getattr(request.state, "request_id", "unknown")
106
+ details = [ErrorSchemas(**error) for error in exc.errors()]
107
+
108
+ # Log where and why validation failed, never the submitted values: exc (and its
109
+ # traceback) embeds the raw request body, which can hold messages, files or tokens.
110
+ logger.warning(
111
+ "Request %s - validation failed: %s %s: %s",
112
+ request_id,
113
+ request.method,
114
+ request.url.path,
115
+ [f"{'.'.join(map(str, d.loc))}: {d.msg}" for d in details],
116
+ )
117
+
118
+ if is_production:
119
+ message = "The request data is invalid. Please check your input."
120
+ else:
121
+ message = str(exc.body) if exc.body else "Validation error"
122
+
123
+ return error_response(
124
+ request,
125
+ error_code="VALIDATION_ERROR",
126
+ message=message,
127
+ details=details if not is_production else None,
128
+ status_code=422,
129
+ )
130
+
131
+ @app.exception_handler(ValueError)
132
+ async def value_exception_handler(request: Request, exc: ValueError):
133
+ logger.error(f"Value error exception: url: {request.base_url}", exc_info=exc)
134
+
135
+ request_id = getattr(request.state, "request_id", "unknown")
136
+
137
+ message = str(exc)
138
+ if is_production:
139
+ logger.error(f"Request {request_id} - ValueError details: {message}")
140
+ message = "Invalid input provided."
141
+
142
+ return error_response(
143
+ request,
144
+ error_code="VALIDATION_ERROR",
145
+ message=message,
146
+ status_code=422,
147
+ )
148
+
149
+ ########################################
150
+ ##### Custom exception handler here ####
151
+ ########################################
152
+ @app.exception_handler(UserAccountError)
153
+ async def user_account_exception_handler(request: Request, exc: UserAccountError):
154
+ logger.warning("UserAccountError on %s: %s", request.url.path, exc.error_code)
155
+ response = error_response(
156
+ request,
157
+ error_code=exc.error_code,
158
+ message=exc.message,
159
+ status_code=exc.status_code,
160
+ )
161
+ if exc.status_code == 401: # noqa: PLR2004
162
+ response.headers["WWW-Authenticate"] = 'Bearer realm="auth_required"'
163
+ return response
164
+
165
+ @app.exception_handler(UserPermissionError)
166
+ async def user_write_exception_handler(request: Request, exc: UserPermissionError):
167
+ logger.error(f"UserPermissionError: url: {request.base_url}", exc_info=exc)
168
+ return error_response(
169
+ request,
170
+ error_code=exc.error_code,
171
+ message=exc.message,
172
+ status_code=exc.status_code,
173
+ )
174
+
175
+ @app.exception_handler(APIResourceNotFoundError)
176
+ async def resource_not_found_exception_handler(request: Request, exc: APIResourceNotFoundError):
177
+ logger.error(f"ResourceNotFoundError: url: {request.base_url}", exc_info=exc)
178
+ return error_response(
179
+ request,
180
+ error_code=exc.error_code,
181
+ message=exc.message,
182
+ status_code=exc.status_code,
183
+ )
184
+
185
+ ## Need to handle 10xGraph specific exceptions here
186
+ @app.exception_handler(ValidationError)
187
+ async def graph_validation_exception_handler(request: Request, exc: ValidationError):
188
+ logger.error(f"10xGraph ValidationError: url: {request.base_url}", exc_info=exc)
189
+
190
+ request_id = getattr(request.state, "request_id", "unknown")
191
+ message = _sanitize_error_message(str(exc), "AGENTFLOW_VALIDATION_ERROR", is_production)
192
+
193
+ if is_production:
194
+ logger.error(f"Request {request_id} - 10xGraph ValidationError: {exc}")
195
+
196
+ return error_response(
197
+ request,
198
+ error_code="AGENTFLOW_VALIDATION_ERROR",
199
+ message=message,
200
+ status_code=422,
201
+ )
202
+
203
+ @app.exception_handler(GraphError)
204
+ async def graph_error_exception_handler(request: Request, exc: GraphError):
205
+ logger.error(f"GraphError: url: {request.base_url}", exc_info=exc)
206
+
207
+ request_id = getattr(request.state, "request_id", "unknown")
208
+ error_code = getattr(exc, "error_code", "GRAPH_000")
209
+ original_message = getattr(exc, "message", str(exc))
210
+ message = _sanitize_error_message(original_message, error_code, is_production)
211
+
212
+ if is_production:
213
+ logger.error(f"Request {request_id} - GraphError: {original_message}")
214
+
215
+ return error_response(
216
+ request,
217
+ error_code=error_code,
218
+ message=message,
219
+ details=getattr(exc, "context", None) if not is_production else None,
220
+ status_code=500,
221
+ )
222
+
223
+ @app.exception_handler(NodeError)
224
+ async def node_error_exception_handler(request: Request, exc: NodeError):
225
+ logger.error(f"NodeError: url: {request.base_url}", exc_info=exc)
226
+
227
+ request_id = getattr(request.state, "request_id", "unknown")
228
+ error_code = getattr(exc, "error_code", "NODE_000")
229
+ original_message = getattr(exc, "message", str(exc))
230
+ message = _sanitize_error_message(original_message, error_code, is_production)
231
+
232
+ if is_production:
233
+ logger.error(f"Request {request_id} - NodeError: {original_message}")
234
+
235
+ return error_response(
236
+ request,
237
+ error_code=error_code,
238
+ message=message,
239
+ details=getattr(exc, "context", None) if not is_production else None,
240
+ status_code=500,
241
+ )
242
+
243
+ @app.exception_handler(GraphRecursionError)
244
+ async def graph_recursion_error_exception_handler(request: Request, exc: GraphRecursionError):
245
+ logger.error(f"GraphRecursionError: url: {request.base_url}", exc_info=exc)
246
+
247
+ request_id = getattr(request.state, "request_id", "unknown")
248
+ error_code = getattr(exc, "error_code", "GRAPH_RECURSION_000")
249
+ original_message = getattr(exc, "message", str(exc))
250
+ message = _sanitize_error_message(original_message, error_code, is_production)
251
+
252
+ if is_production:
253
+ logger.error(f"Request {request_id} - GraphRecursionError: {original_message}")
254
+
255
+ return error_response(
256
+ request,
257
+ error_code=error_code,
258
+ message=message,
259
+ details=getattr(exc, "context", None) if not is_production else None,
260
+ status_code=500,
261
+ )
262
+
263
+ @app.exception_handler(StorageError)
264
+ async def storage_error_exception_handler(request: Request, exc: StorageError):
265
+ logger.error(f"StorageError: url: {request.base_url}", exc_info=exc)
266
+
267
+ request_id = getattr(request.state, "request_id", "unknown")
268
+ error_code = getattr(exc, "error_code", "STORAGE_000")
269
+ original_message = getattr(exc, "message", str(exc))
270
+ message = _sanitize_error_message(original_message, error_code, is_production)
271
+
272
+ if is_production:
273
+ logger.error(f"Request {request_id} - StorageError: {original_message}")
274
+
275
+ return error_response(
276
+ request,
277
+ error_code=error_code,
278
+ message=message,
279
+ details=getattr(exc, "context", None) if not is_production else None,
280
+ status_code=500,
281
+ )
282
+
283
+ @app.exception_handler(TransientStorageError)
284
+ async def transient_storage_error_exception_handler(
285
+ request: Request, exc: TransientStorageError
286
+ ):
287
+ logger.error(f"TransientStorageError: url: {request.base_url}", exc_info=exc)
288
+
289
+ request_id = getattr(request.state, "request_id", "unknown")
290
+ error_code = getattr(exc, "error_code", "TRANSIENT_STORAGE_000")
291
+ original_message = getattr(exc, "message", str(exc))
292
+ message = _sanitize_error_message(original_message, error_code, is_production)
293
+
294
+ if is_production:
295
+ logger.error(f"Request {request_id} - TransientStorageError: {original_message}")
296
+
297
+ return error_response(
298
+ request,
299
+ error_code=error_code,
300
+ message=message,
301
+ details=getattr(exc, "context", None) if not is_production else None,
302
+ status_code=503,
303
+ )
304
+
305
+ @app.exception_handler(MetricsError)
306
+ async def metrics_error_exception_handler(request: Request, exc: MetricsError):
307
+ logger.error(f"MetricsError: url: {request.base_url}", exc_info=exc)
308
+
309
+ request_id = getattr(request.state, "request_id", "unknown")
310
+ error_code = getattr(exc, "error_code", "METRICS_000")
311
+ original_message = getattr(exc, "message", str(exc))
312
+ message = _sanitize_error_message(original_message, error_code, is_production)
313
+
314
+ if is_production:
315
+ logger.error(f"Request {request_id} - MetricsError: {original_message}")
316
+
317
+ return error_response(
318
+ request,
319
+ error_code=error_code,
320
+ message=message,
321
+ details=getattr(exc, "context", None) if not is_production else None,
322
+ status_code=500,
323
+ )
324
+
325
+ @app.exception_handler(SchemaVersionError)
326
+ async def schema_version_error_exception_handler(request: Request, exc: SchemaVersionError):
327
+ logger.error(f"SchemaVersionError: url: {request.base_url}", exc_info=exc)
328
+
329
+ request_id = getattr(request.state, "request_id", "unknown")
330
+ error_code = getattr(exc, "error_code", "SCHEMA_VERSION_000")
331
+ original_message = getattr(exc, "message", str(exc))
332
+ message = _sanitize_error_message(original_message, error_code, is_production)
333
+
334
+ if is_production:
335
+ logger.error(f"Request {request_id} - SchemaVersionError: {original_message}")
336
+
337
+ return error_response(
338
+ request,
339
+ error_code=error_code,
340
+ message=message,
341
+ details=getattr(exc, "context", None) if not is_production else None,
342
+ status_code=422,
343
+ )
344
+
345
+ @app.exception_handler(SerializationError)
346
+ async def serialization_error_exception_handler(request: Request, exc: SerializationError):
347
+ logger.error(f"SerializationError: url: {request.base_url}", exc_info=exc)
348
+
349
+ request_id = getattr(request.state, "request_id", "unknown")
350
+ error_code = getattr(exc, "error_code", "SERIALIZATION_000")
351
+ original_message = getattr(exc, "message", str(exc))
352
+ message = _sanitize_error_message(original_message, error_code, is_production)
353
+
354
+ if is_production:
355
+ logger.error(f"Request {request_id} - SerializationError: {original_message}")
356
+
357
+ return error_response(
358
+ request,
359
+ error_code=error_code,
360
+ message=message,
361
+ details=getattr(exc, "context", None) if not is_production else None,
362
+ status_code=500,
363
+ )
@@ -0,0 +1,72 @@
1
+ from .general_exception import GeneralException
2
+
3
+
4
+ class ResourceNotFoundError(GeneralException):
5
+ """
6
+ Exception raised when a requested resource is not found.
7
+
8
+ Attributes:
9
+ message (str): Explanation of the error.
10
+ status_code (int): HTTP status code for the error.
11
+ error_code (str): Specific error code for the error.
12
+ """
13
+
14
+ # Implementation here
15
+ def __init__(self, message="Resources not found"):
16
+ self.message = message
17
+ self.status_code = 404
18
+ self.error_code = "RESOURCE_NOT_FOUND"
19
+ super().__init__(
20
+ message=self.message,
21
+ status_code=self.status_code,
22
+ error_code=self.error_code,
23
+ )
24
+
25
+
26
+ class ResourceDuplicationError(GeneralException):
27
+ """
28
+ Exception raised for duplicate resource requests.
29
+
30
+ This exception is used to indicate that a request has been made for a resource
31
+ that already exists, and such duplication is not allowed.
32
+
33
+ Attributes:
34
+ message (str): Explanation of the error. Defaults to "Duplicate request".
35
+ status_code (int): HTTP status code for the error. Defaults to 403.
36
+ error_code (str): Specific error code for duplicate requests.
37
+ Defaults to "DUPLICATE_REQUEST".
38
+ """
39
+
40
+ def __init__(self, message="Duplicate request"):
41
+ self.message = message
42
+ self.status_code = 403
43
+ self.error_code = "DUPLICATE_REQUEST"
44
+ super().__init__(
45
+ message=self.message,
46
+ status_code=self.status_code,
47
+ error_code=self.error_code,
48
+ )
49
+
50
+
51
+ class InvalidOperationError(GeneralException):
52
+ """
53
+ Exception raised for invalid operations.
54
+
55
+ This exception is used to indicate that a particular operation is not supported.
56
+
57
+ Attributes:
58
+ message (str): Explanation of the error. Defaults to "Operation Not Supported".
59
+ status_code (int): HTTP status code for the error. Defaults to 403.
60
+ error_code (str): Specific error code for the exception.
61
+ Defaults to "InvalidOperationError".
62
+ """
63
+
64
+ def __init__(self, message="Operation Not Supported"):
65
+ self.message = message
66
+ self.status_code = 403
67
+ self.error_code = "InvalidOperationError"
68
+ super().__init__(
69
+ message=self.message,
70
+ status_code=self.status_code,
71
+ error_code=self.error_code,
72
+ )
@@ -0,0 +1,58 @@
1
+ from .general_exception import GeneralException
2
+
3
+
4
+ class UserAccountError(GeneralException):
5
+ """
6
+ Exception raised for errors related to user account status.
7
+
8
+ This exception is raised when a user account is disabled and
9
+ the user needs to contact support.
10
+
11
+ Attributes:
12
+ message (str): Explanation of the error. Defaults to "User account is disabled,
13
+ please contact support".
14
+ error_code (str): Specific error code for this type of error.
15
+ Defaults to "USER_ACCOUNT_DISABLE".
16
+ status_code (int): HTTP status code for the error. Defaults to 403; a missing,
17
+ invalid or expired credential uses 401.
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ message="User account is disabled, please contact support",
23
+ error_code="USER_ACCOUNT_DISABLE",
24
+ status_code=403,
25
+ ):
26
+ self.message = message
27
+ self.status_code = status_code
28
+ self.error_code = error_code
29
+ super().__init__(
30
+ message=self.message,
31
+ status_code=self.status_code,
32
+ error_code=self.error_code,
33
+ )
34
+
35
+
36
+ class UserPermissionError(GeneralException):
37
+ """
38
+ Exception raised for user permission errors.
39
+
40
+ Attributes:
41
+ message (str): Explanation of the error.
42
+ status_code (int): HTTP status code for the error.
43
+ error_code (str): Specific error code for the permission error.
44
+
45
+ Args:
46
+ message (str, optional): Custom error message.
47
+ Defaults to "user don't have sufficient permission".
48
+ """
49
+
50
+ def __init__(self, message="user don't have sufficient permission"):
51
+ self.message = message
52
+ self.status_code = 403
53
+ self.error_code = "PERMISSION_ERROR"
54
+ super().__init__(
55
+ message=self.message,
56
+ status_code=self.status_code,
57
+ error_code=self.error_code,
58
+ )
@@ -0,0 +1,25 @@
1
+ """Middleware modules for tenxgraph_api."""
2
+
3
+ from .rate_limit import (
4
+ BaseRateLimitBackend,
5
+ MemoryRateLimitBackend,
6
+ RateLimitDecision,
7
+ RateLimitMiddleware,
8
+ RedisRateLimitBackend,
9
+ build_backend,
10
+ )
11
+ from .request_limits import RequestSizeLimitMiddleware
12
+ from .security_headers import SecurityHeadersMiddleware, create_security_headers_middleware
13
+
14
+
15
+ __all__ = [
16
+ "BaseRateLimitBackend",
17
+ "RateLimitMiddleware",
18
+ "RateLimitDecision",
19
+ "MemoryRateLimitBackend",
20
+ "RedisRateLimitBackend",
21
+ "build_backend",
22
+ "RequestSizeLimitMiddleware",
23
+ "SecurityHeadersMiddleware",
24
+ "create_security_headers_middleware",
25
+ ]
@@ -0,0 +1,15 @@
1
+ from .base import BaseRateLimitBackend, RateLimitDecision
2
+ from .factory import build_backend
3
+ from .memory import MemoryRateLimitBackend
4
+ from .middleware import RateLimitMiddleware
5
+ from .redis import RedisRateLimitBackend
6
+
7
+
8
+ __all__ = [
9
+ "BaseRateLimitBackend",
10
+ "MemoryRateLimitBackend",
11
+ "RateLimitDecision",
12
+ "RateLimitMiddleware",
13
+ "RedisRateLimitBackend",
14
+ "build_backend",
15
+ ]
@@ -0,0 +1,27 @@
1
+ from abc import ABC, abstractmethod
2
+ from dataclasses import dataclass
3
+
4
+
5
+ @dataclass(frozen=True)
6
+ class RateLimitDecision:
7
+ """Result of a single rate-limit check."""
8
+
9
+ allowed: bool
10
+ remaining: int
11
+ reset_after: int
12
+
13
+
14
+ class BaseRateLimitBackend(ABC):
15
+ """Abstract base class for rate-limit backends.
16
+
17
+ Users can implement this class and bind an instance into InjectQ when they
18
+ need a backend other than the built-in memory or Redis implementations.
19
+ """
20
+
21
+ @abstractmethod
22
+ async def check(self, key: str, *, limit: int, window: int) -> RateLimitDecision:
23
+ """Atomically check and record a request for *key*."""
24
+
25
+ @abstractmethod
26
+ async def close(self) -> None:
27
+ """Release resources held by the backend, if any."""
@@ -0,0 +1,98 @@
1
+ import logging
2
+ from typing import TYPE_CHECKING, Any
3
+
4
+ from injectq import InjectQ
5
+
6
+ from tenxgraph_api.src.app.core.config.graph_config import RateLimitConfig
7
+
8
+ from .base import BaseRateLimitBackend
9
+ from .memory import MemoryRateLimitBackend
10
+ from .redis import RedisRateLimitBackend
11
+
12
+
13
+ if TYPE_CHECKING:
14
+ from redis.asyncio import Redis
15
+
16
+
17
+ logger = logging.getLogger("tenxgraph_api.rate_limit")
18
+
19
+
20
+ def build_backend(
21
+ config: RateLimitConfig,
22
+ container: InjectQ | None = None,
23
+ ) -> BaseRateLimitBackend:
24
+ """Build or resolve the configured rate-limit backend.
25
+
26
+ Custom backends are provided by binding a ``BaseRateLimitBackend`` instance
27
+ into InjectQ, matching the style used by auth/authorization.
28
+ """
29
+ container = container or InjectQ.get_instance()
30
+
31
+ injected_backend = container.try_get(BaseRateLimitBackend)
32
+ if config.backend == "custom":
33
+ if not injected_backend:
34
+ raise ValueError(
35
+ "rate_limit.backend='custom' requires a BaseRateLimitBackend "
36
+ "instance bound in InjectQ"
37
+ )
38
+ return injected_backend
39
+
40
+ if config.backend == "redis":
41
+ return _build_redis_backend(config, container)
42
+
43
+ logger.info("Rate-limit backend: memory (in-process, not shared across workers)")
44
+ return MemoryRateLimitBackend()
45
+
46
+
47
+ def _build_redis_backend(
48
+ config: RateLimitConfig,
49
+ container: InjectQ,
50
+ ) -> RedisRateLimitBackend:
51
+ redis = _get_redis_from_container(container)
52
+ if redis is not None:
53
+ logger.info(
54
+ "Rate-limit backend: Redis from InjectQ (prefix=%s, fail_open=%s)",
55
+ config.redis_prefix,
56
+ config.fail_open,
57
+ )
58
+ return RedisRateLimitBackend(
59
+ redis=redis,
60
+ prefix=config.redis_prefix,
61
+ fail_open=config.fail_open,
62
+ close_redis=False,
63
+ )
64
+
65
+ if not config.redis_url:
66
+ raise ValueError("rate_limit.redis.url is required when no Redis client is bound")
67
+
68
+ backend = RedisRateLimitBackend.from_url(
69
+ redis_url=config.redis_url,
70
+ prefix=config.redis_prefix,
71
+ fail_open=config.fail_open,
72
+ )
73
+ _bind_redis(container, backend._redis)
74
+ logger.info(
75
+ "Rate-limit backend: created Redis and bound it in InjectQ (prefix=%s, fail_open=%s)",
76
+ config.redis_prefix,
77
+ config.fail_open,
78
+ )
79
+ return backend
80
+
81
+
82
+ def _get_redis_from_container(container: InjectQ) -> Any | None:
83
+ redis = container.try_get("redis")
84
+ if redis is not None:
85
+ return redis
86
+
87
+ redis = container.try_get("redis_client")
88
+ if redis is not None:
89
+ return redis
90
+
91
+ return container.try_get("Redis")
92
+
93
+
94
+ def _bind_redis(container: InjectQ, redis: "Redis") -> None:
95
+ if Redis is not None:
96
+ container.bind_instance(Redis, redis)
97
+ container.bind_instance("redis", redis)
98
+ container.bind_instance("redis_client", redis)