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.
- 10xgraph_api-0.7.0.dist-info/METADATA +618 -0
- 10xgraph_api-0.7.0.dist-info/RECORD +203 -0
- 10xgraph_api-0.7.0.dist-info/WHEEL +5 -0
- 10xgraph_api-0.7.0.dist-info/entry_points.txt +3 -0
- 10xgraph_api-0.7.0.dist-info/licenses/LICENSE +21 -0
- 10xgraph_api-0.7.0.dist-info/top_level.txt +2 -0
- agentflow_cli/__init__.py +69 -0
- tenxgraph_api/__init__.py +23 -0
- tenxgraph_api/cli/__init__.py +6 -0
- tenxgraph_api/cli/capabilities.py +127 -0
- tenxgraph_api/cli/commands/__init__.py +49 -0
- tenxgraph_api/cli/commands/api.py +262 -0
- tenxgraph_api/cli/commands/audit.py +213 -0
- tenxgraph_api/cli/commands/build.py +305 -0
- tenxgraph_api/cli/commands/config.py +71 -0
- tenxgraph_api/cli/commands/demo.py +93 -0
- tenxgraph_api/cli/commands/eval.py +909 -0
- tenxgraph_api/cli/commands/init.py +531 -0
- tenxgraph_api/cli/commands/skills.py +532 -0
- tenxgraph_api/cli/commands/test.py +113 -0
- tenxgraph_api/cli/commands/version.py +47 -0
- tenxgraph_api/cli/config_editor/__init__.py +15 -0
- tenxgraph_api/cli/config_editor/schema.py +566 -0
- tenxgraph_api/cli/config_editor/server.py +240 -0
- tenxgraph_api/cli/config_editor/static/app.css +2 -0
- tenxgraph_api/cli/config_editor/static/app.js +3 -0
- tenxgraph_api/cli/config_editor/static/favicon.svg +11 -0
- tenxgraph_api/cli/config_editor/static/index.html +20 -0
- tenxgraph_api/cli/config_editor/store.py +120 -0
- tenxgraph_api/cli/config_editor/validation.py +377 -0
- tenxgraph_api/cli/constants.py +112 -0
- tenxgraph_api/cli/context.py +46 -0
- tenxgraph_api/cli/core/__init__.py +0 -0
- tenxgraph_api/cli/core/animations.py +550 -0
- tenxgraph_api/cli/core/config.py +279 -0
- tenxgraph_api/cli/core/output.py +625 -0
- tenxgraph_api/cli/core/prompts.py +220 -0
- tenxgraph_api/cli/core/screen.py +287 -0
- tenxgraph_api/cli/core/steps.py +621 -0
- tenxgraph_api/cli/core/theme.py +242 -0
- tenxgraph_api/cli/core/validation.py +251 -0
- tenxgraph_api/cli/exceptions.py +129 -0
- tenxgraph_api/cli/logger.py +84 -0
- tenxgraph_api/cli/main.py +936 -0
- tenxgraph_api/cli/templates/__init__.py +0 -0
- tenxgraph_api/cli/templates/defaults.py +844 -0
- tenxgraph_api/cli/templates/dev/.env.example +1 -0
- tenxgraph_api/cli/templates/dev/10xgraph.json +16 -0
- tenxgraph_api/cli/templates/dev/graph/__init__.py +0 -0
- tenxgraph_api/cli/templates/dev/graph/agent.py +63 -0
- tenxgraph_api/cli/templates/prod/.env.example +75 -0
- tenxgraph_api/cli/templates/prod/.pre-commit-config.yaml +50 -0
- tenxgraph_api/cli/templates/prod/.python-version +1 -0
- tenxgraph_api/cli/templates/prod/10xgraph.json +42 -0
- tenxgraph_api/cli/templates/prod/auth/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/auth/agent_auth.py +46 -0
- tenxgraph_api/cli/templates/prod/evals/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/evals/user_simulator_eval.py +76 -0
- tenxgraph_api/cli/templates/prod/evals/weather_agents_eval.py +53 -0
- tenxgraph_api/cli/templates/prod/graph/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/graph/agent.py +50 -0
- tenxgraph_api/cli/templates/prod/graph/state.py +5 -0
- tenxgraph_api/cli/templates/prod/graph/thread_name_generator.py +7 -0
- tenxgraph_api/cli/templates/prod/graph/tools/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/graph/tools/weather_tool.py +43 -0
- tenxgraph_api/cli/templates/prod/graph/validators/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/graph/validators/lifecyle.py +107 -0
- tenxgraph_api/cli/templates/prod/graph/validators/manager.py +9 -0
- tenxgraph_api/cli/templates/prod/graph/validators/validators.py +21 -0
- tenxgraph_api/cli/templates/prod/pyproject.toml +79 -0
- tenxgraph_api/cli/templates/prod/tests/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/tests/conftest.py +6 -0
- tenxgraph_api/cli/templates/prod/tests/test_agent_eval.py +78 -0
- tenxgraph_api/cli/templates/prod/tests/test_catalog_tools.py +47 -0
- tenxgraph_api/cli/templates/prod/tests/test_graph_nodes.py +57 -0
- tenxgraph_api/cli/templates/skills/10xgraph/SKILL.md +92 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/agents-and-tools.md +389 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/api-client.md +61 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/api-configuration.md +75 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/api-settings-and-middleware.md +89 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/architecture.md +91 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/auth-and-authorization.md +149 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/callbacks-and-command.md +283 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/checkpointing-and-threads.md +63 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/cli-commands.md +73 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/client-auth-and-errors.md +94 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/client-messages-invoke-stream.md +115 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/client-threads-memory-files.md +80 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/context-id-background.md +80 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/dependency-injection.md +58 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/evaluation.md +500 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/id-and-thread-name-generators.md +69 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/media-and-files.md +66 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/memory-and-store.md +70 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/prebuilt-agents-and-tools.md +394 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/production-runtime.md +56 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/providers-and-adapters.md +89 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/publishers-and-runtime-protocols.md +84 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/rate-limiting.md +194 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/realtime.md +172 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/remote-tools.md +55 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/rest-api-and-errors.md +97 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/security-and-validators.md +86 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/state-and-messages.md +68 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/state-graph.md +232 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/stream-emitter.md +144 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/streaming.md +53 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/testing-and-evaluation.md +64 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/unit-testing.md +202 -0
- tenxgraph_api/cli/templates/skills/copilot/10xgraph.instructions.md +108 -0
- tenxgraph_api/py.typed +0 -0
- tenxgraph_api/src/__init__.py +0 -0
- tenxgraph_api/src/app/__init__.py +13 -0
- tenxgraph_api/src/app/core/__init__.py +15 -0
- tenxgraph_api/src/app/core/auth/__init__.py +0 -0
- tenxgraph_api/src/app/core/auth/auth_backend.py +38 -0
- tenxgraph_api/src/app/core/auth/authorization.py +379 -0
- tenxgraph_api/src/app/core/auth/base_auth.py +27 -0
- tenxgraph_api/src/app/core/auth/jwt_auth.py +122 -0
- tenxgraph_api/src/app/core/auth/ownership_resolver.py +170 -0
- tenxgraph_api/src/app/core/auth/permissions.py +354 -0
- tenxgraph_api/src/app/core/auth/request_config.py +130 -0
- tenxgraph_api/src/app/core/auth/route_guard.py +105 -0
- tenxgraph_api/src/app/core/config/__init__.py +0 -0
- tenxgraph_api/src/app/core/config/graph_config.py +532 -0
- tenxgraph_api/src/app/core/config/media_settings.py +72 -0
- tenxgraph_api/src/app/core/config/sentry_config.py +60 -0
- tenxgraph_api/src/app/core/config/settings.py +202 -0
- tenxgraph_api/src/app/core/config/setup_logs.py +65 -0
- tenxgraph_api/src/app/core/config/setup_middleware.py +483 -0
- tenxgraph_api/src/app/core/config/worker_middleware.py +99 -0
- tenxgraph_api/src/app/core/exceptions/__init__.py +17 -0
- tenxgraph_api/src/app/core/exceptions/general_exception.py +58 -0
- tenxgraph_api/src/app/core/exceptions/handle_errors.py +363 -0
- tenxgraph_api/src/app/core/exceptions/resources_exceptions.py +72 -0
- tenxgraph_api/src/app/core/exceptions/user_exception.py +58 -0
- tenxgraph_api/src/app/core/middleware/__init__.py +25 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/__init__.py +15 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/base.py +27 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/factory.py +98 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/keying.py +133 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/memory.py +98 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/middleware.py +90 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/redis.py +115 -0
- tenxgraph_api/src/app/core/middleware/request_limits.py +154 -0
- tenxgraph_api/src/app/core/middleware/security_headers.py +239 -0
- tenxgraph_api/src/app/core/utils/__init__.py +14 -0
- tenxgraph_api/src/app/core/utils/log_sanitizer.py +216 -0
- tenxgraph_api/src/app/loader.py +516 -0
- tenxgraph_api/src/app/main.py +180 -0
- tenxgraph_api/src/app/routers/__init__.py +4 -0
- tenxgraph_api/src/app/routers/ag_ui/__init__.py +10 -0
- tenxgraph_api/src/app/routers/ag_ui/converter.py +195 -0
- tenxgraph_api/src/app/routers/ag_ui/event_mapper.py +353 -0
- tenxgraph_api/src/app/routers/ag_ui/router.py +90 -0
- tenxgraph_api/src/app/routers/ag_ui/service.py +193 -0
- tenxgraph_api/src/app/routers/checkpointer/__init__.py +1 -0
- tenxgraph_api/src/app/routers/checkpointer/router.py +482 -0
- tenxgraph_api/src/app/routers/checkpointer/schemas/__init__.py +1 -0
- tenxgraph_api/src/app/routers/checkpointer/schemas/checkpointer_schemas.py +121 -0
- tenxgraph_api/src/app/routers/checkpointer/services/__init__.py +1 -0
- tenxgraph_api/src/app/routers/checkpointer/services/checkpointer_service.py +300 -0
- tenxgraph_api/src/app/routers/evals/__init__.py +6 -0
- tenxgraph_api/src/app/routers/evals/router.py +38 -0
- tenxgraph_api/src/app/routers/evals/services/__init__.py +1 -0
- tenxgraph_api/src/app/routers/evals/services/eval_report_service.py +354 -0
- tenxgraph_api/src/app/routers/graph/__init__.py +6 -0
- tenxgraph_api/src/app/routers/graph/realtime_guard.py +161 -0
- tenxgraph_api/src/app/routers/graph/router.py +821 -0
- tenxgraph_api/src/app/routers/graph/schemas/__init__.py +1 -0
- tenxgraph_api/src/app/routers/graph/schemas/graph_schemas.py +386 -0
- tenxgraph_api/src/app/routers/graph/services/__init__.py +1 -0
- tenxgraph_api/src/app/routers/graph/services/graph_service.py +1219 -0
- tenxgraph_api/src/app/routers/graph/services/multimodal_preprocessor.py +164 -0
- tenxgraph_api/src/app/routers/media/__init__.py +506 -0
- tenxgraph_api/src/app/routers/media/router.py +242 -0
- tenxgraph_api/src/app/routers/media/schemas.py +39 -0
- tenxgraph_api/src/app/routers/ping/__init__.py +4 -0
- tenxgraph_api/src/app/routers/ping/router.py +28 -0
- tenxgraph_api/src/app/routers/setup_router.py +60 -0
- tenxgraph_api/src/app/routers/store/__init__.py +4 -0
- tenxgraph_api/src/app/routers/store/router.py +200 -0
- tenxgraph_api/src/app/routers/store/schemas/__init__.py +28 -0
- tenxgraph_api/src/app/routers/store/schemas/store_schemas.py +200 -0
- tenxgraph_api/src/app/routers/store/services/__init__.py +4 -0
- tenxgraph_api/src/app/routers/store/services/store_service.py +203 -0
- tenxgraph_api/src/app/tasks/__init__.py +0 -0
- tenxgraph_api/src/app/tasks/user_tasks.py +38 -0
- tenxgraph_api/src/app/utils/__init__.py +12 -0
- tenxgraph_api/src/app/utils/callable_helper.py +26 -0
- tenxgraph_api/src/app/utils/media/__init__.py +15 -0
- tenxgraph_api/src/app/utils/media/extractor.py +172 -0
- tenxgraph_api/src/app/utils/media/pipeline.py +83 -0
- tenxgraph_api/src/app/utils/parse_output.py +17 -0
- tenxgraph_api/src/app/utils/response_helper.py +129 -0
- tenxgraph_api/src/app/utils/schemas/__init__.py +11 -0
- tenxgraph_api/src/app/utils/schemas/output_schemas.py +28 -0
- tenxgraph_api/src/app/utils/schemas/user_schemas.py +13 -0
- tenxgraph_api/src/app/utils/snowflake_id_generator.py +90 -0
- tenxgraph_api/src/app/utils/swagger_helper.py +160 -0
- tenxgraph_api/src/app/utils/telemetry_store.py +123 -0
- tenxgraph_api/src/app/utils/thread_name_generator.py +304 -0
- tenxgraph_api/src/app/worker.py +91 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
from fastapi import FastAPI
|
|
5
|
+
from fastapi.middleware.gzip import GZipMiddleware
|
|
6
|
+
from fastapi.middleware.trustedhost import TrustedHostMiddleware
|
|
7
|
+
from fastapi.responses import JSONResponse
|
|
8
|
+
from injectq import InjectQ
|
|
9
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
10
|
+
from starlette.middleware.cors import CORSMiddleware
|
|
11
|
+
from starlette.requests import Request
|
|
12
|
+
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
13
|
+
|
|
14
|
+
from tenxgraph_api.src.app.core.middleware.rate_limit import RateLimitMiddleware, build_backend
|
|
15
|
+
from tenxgraph_api.src.app.core.middleware.request_limits import RequestSizeLimitMiddleware
|
|
16
|
+
from tenxgraph_api.src.app.core.middleware.security_headers import SecurityHeadersMiddleware
|
|
17
|
+
|
|
18
|
+
from .graph_config import GraphConfig
|
|
19
|
+
from .sentry_config import init_sentry
|
|
20
|
+
from .settings import get_settings, logger
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class HealthCheckAwareTrustedHostMiddleware(TrustedHostMiddleware):
|
|
24
|
+
"""``TrustedHostMiddleware`` that lets the health check through from any Host.
|
|
25
|
+
|
|
26
|
+
Docker and Kubernetes probes call ``/ping`` on ``localhost`` or the pod IP, which are
|
|
27
|
+
never in ``ALLOWED_HOST``, so the probe would fail and the container would be restarted.
|
|
28
|
+
``/ping`` returns a constant and reads nothing, so skipping the Host check there is safe.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
32
|
+
if scope["type"] == "http" and _is_health_check(scope):
|
|
33
|
+
await self.app(scope, receive, send)
|
|
34
|
+
return
|
|
35
|
+
await super().__call__(scope, receive, send)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Multipart framing (boundaries, part headers, the filename field) on top of the file bytes.
|
|
39
|
+
UPLOAD_MULTIPART_OVERHEAD_BYTES = 64 * 1024
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _upload_path_limits(max_request_size: int) -> dict[str, int]:
|
|
43
|
+
"""A body limit for the upload route that fits ``MEDIA_MAX_SIZE_MB``, if that is larger."""
|
|
44
|
+
from tenxgraph_api.src.app.core.config.media_settings import get_media_settings
|
|
45
|
+
|
|
46
|
+
media_bytes = int(get_media_settings().MEDIA_MAX_SIZE_MB * 1024 * 1024)
|
|
47
|
+
upload_limit = media_bytes + UPLOAD_MULTIPART_OVERHEAD_BYTES
|
|
48
|
+
if upload_limit <= max_request_size:
|
|
49
|
+
return {}
|
|
50
|
+
return {"/v1/files/upload": upload_limit}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _is_health_check(scope: Scope) -> bool:
|
|
54
|
+
path = scope.get("path", "")
|
|
55
|
+
root_path = (scope.get("root_path") or "").rstrip("/")
|
|
56
|
+
return path in ("/ping", f"{root_path}/ping")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Paths that should be excluded from GZip compression (streaming endpoints)
|
|
60
|
+
GZIP_EXCLUDED_PATHS = frozenset({"/v1/graph/stream"})
|
|
61
|
+
|
|
62
|
+
# StaleStateError is raised by checkpointers that do optimistic concurrency
|
|
63
|
+
# control on state writes. It was added in a later core release, and this package
|
|
64
|
+
# supports 10xgraph>=0.7.0, so treat it as optional: when the installed
|
|
65
|
+
# core does not expose it, we simply do not register the 409 handler.
|
|
66
|
+
try:
|
|
67
|
+
from tenxgraph.core.exceptions import StaleStateError
|
|
68
|
+
except ImportError: # pragma: no cover - depends on installed core version
|
|
69
|
+
StaleStateError = None # type: ignore[assignment]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class InsecureCorsConfigError(RuntimeError):
|
|
73
|
+
"""Raised when production is configured with wildcard CORS + credentials."""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _resolve_cors_policy(settings) -> tuple[list[str], bool]:
|
|
77
|
+
"""Decide the CORS origins/credentials policy, failing closed in production.
|
|
78
|
+
|
|
79
|
+
``ORIGINS="*"`` on its own is a legitimate choice for a public, token-less
|
|
80
|
+
API, and a permissive default is normal in development. The dangerous case is
|
|
81
|
+
wildcard origins *combined with credentials*: Starlette reflects the caller's
|
|
82
|
+
Origin header back alongside ``Access-Control-Allow-Credentials: true``, which
|
|
83
|
+
turns every origin into a trusted, credentialed one. Warning about it (the old
|
|
84
|
+
behaviour) still served it.
|
|
85
|
+
|
|
86
|
+
In production that combination is refused outright, with two ways forward:
|
|
87
|
+
set explicit ``ORIGINS``, or set ``CORS_ALLOW_CREDENTIALS=false``.
|
|
88
|
+
"""
|
|
89
|
+
origins = [o.strip() for o in settings.ORIGINS.split(",") if o.strip()]
|
|
90
|
+
allow_credentials = bool(getattr(settings, "CORS_ALLOW_CREDENTIALS", True))
|
|
91
|
+
is_wildcard = "*" in origins
|
|
92
|
+
is_production = str(getattr(settings, "MODE", "development")).lower() == "production"
|
|
93
|
+
|
|
94
|
+
if is_wildcard and allow_credentials:
|
|
95
|
+
if is_production:
|
|
96
|
+
raise InsecureCorsConfigError(
|
|
97
|
+
"Refusing to start: CORS is configured with ORIGINS='*' and "
|
|
98
|
+
"credentials enabled while MODE=production. This reflects any "
|
|
99
|
+
"caller's Origin back with Access-Control-Allow-Credentials: true, "
|
|
100
|
+
"making every origin a trusted one. Fix by either:\n"
|
|
101
|
+
" 1. setting ORIGINS to an explicit comma-separated list, or\n"
|
|
102
|
+
" 2. setting CORS_ALLOW_CREDENTIALS=false for a public, "
|
|
103
|
+
"non-credentialed API."
|
|
104
|
+
)
|
|
105
|
+
logger.warning(
|
|
106
|
+
"CORS is wildcard ('*') with credentials enabled. This is accepted in "
|
|
107
|
+
"development but will refuse to start when MODE=production."
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return origins, allow_credentials
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
async def _stale_state_handler(request: Request, exc: Exception) -> JSONResponse:
|
|
114
|
+
"""Return 409 Conflict when a run loses the checkpointer's version check.
|
|
115
|
+
|
|
116
|
+
A ``StaleStateError`` means another execution committed a newer state for the
|
|
117
|
+
same thread while this one was running, so persisting would silently discard
|
|
118
|
+
that work. This is a genuine conflict, not a server fault: surface it as 409
|
|
119
|
+
so the client can reload the thread and retry instead of seeing a 500.
|
|
120
|
+
"""
|
|
121
|
+
logger.warning("State conflict on concurrent run: %s", exc)
|
|
122
|
+
context = getattr(exc, "context", {}) or {}
|
|
123
|
+
return JSONResponse(
|
|
124
|
+
status_code=409,
|
|
125
|
+
content={
|
|
126
|
+
"error": "state_conflict",
|
|
127
|
+
"detail": (
|
|
128
|
+
"This thread was updated by another run while yours was in flight. "
|
|
129
|
+
"Reload the thread and retry."
|
|
130
|
+
),
|
|
131
|
+
"thread_id": context.get("thread_id"),
|
|
132
|
+
},
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class SelectiveGZipMiddleware:
|
|
137
|
+
"""
|
|
138
|
+
GZip middleware that excludes certain paths from compression.
|
|
139
|
+
|
|
140
|
+
This is necessary because streaming endpoints need to send data
|
|
141
|
+
immediately without buffering, but GZipMiddleware buffers the
|
|
142
|
+
entire response before compressing.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
def __init__(self, app: ASGIApp, minimum_size: int = 1000):
|
|
146
|
+
self.app = app
|
|
147
|
+
self.gzip_app = GZipMiddleware(app, minimum_size=minimum_size)
|
|
148
|
+
|
|
149
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
150
|
+
if scope["type"] == "http" and scope["path"] in GZIP_EXCLUDED_PATHS:
|
|
151
|
+
# Skip GZip for excluded paths - pass through directly
|
|
152
|
+
await self.app(scope, receive, send)
|
|
153
|
+
else:
|
|
154
|
+
# Apply GZip for all other paths
|
|
155
|
+
await self.gzip_app(scope, receive, send)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class RequestIDMiddleware(BaseHTTPMiddleware):
|
|
159
|
+
"""
|
|
160
|
+
Middleware to add a unique request ID and timestamp to each request and response.
|
|
161
|
+
|
|
162
|
+
This middleware generates a unique request ID and a timestamp when a request is received.
|
|
163
|
+
It adds these values to the request state and includes them in the response headers.
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
Methods:
|
|
167
|
+
dispatch(request: Request, call_next):
|
|
168
|
+
Generates a unique request ID and timestamp, adds them to the request state,
|
|
169
|
+
and includes them in the response headers.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Response: The HTTP response with added request ID and timestamp headers.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
async def dispatch(self, request: Request, call_next):
|
|
176
|
+
"""
|
|
177
|
+
Middleware dispatch method to handle incoming requests.
|
|
178
|
+
|
|
179
|
+
This method generates a unique request ID and a timestamp for each incoming request,
|
|
180
|
+
adds them to the request state, and includes them in the response headers for
|
|
181
|
+
logging purposes.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
request (Request): The incoming HTTP request.
|
|
185
|
+
call_next (Callable): The next middleware or route handler to be called.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Response: The HTTP response with added headers for request ID and timestamp.
|
|
189
|
+
"""
|
|
190
|
+
# Generate request ID and timestamp
|
|
191
|
+
request_id = str(uuid.uuid4())
|
|
192
|
+
timestamp = datetime.now().isoformat()
|
|
193
|
+
|
|
194
|
+
# Add request ID and timestamp to request headers
|
|
195
|
+
request.state.request_id = request_id
|
|
196
|
+
request.state.timestamp = timestamp
|
|
197
|
+
logger.debug(f"Requesting: Request ID: {request_id}, Timestamp: {timestamp}")
|
|
198
|
+
|
|
199
|
+
# Proceed with the request
|
|
200
|
+
response = await call_next(request)
|
|
201
|
+
|
|
202
|
+
# Add request ID and timestamp to response headers for logging
|
|
203
|
+
response.headers["X-Request-ID"] = request_id
|
|
204
|
+
response.headers["X-Timestamp"] = timestamp
|
|
205
|
+
logger.debug(f"Response: Request ID: {request_id}, Timestamp: {timestamp}")
|
|
206
|
+
|
|
207
|
+
return response
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _attach_otel_publisher(container: InjectQ, settings) -> None:
|
|
211
|
+
"""Wire OtelPublisher into the container when OTEL_ENABLED=true.
|
|
212
|
+
|
|
213
|
+
Merges with any publisher already bound in the container:
|
|
214
|
+
- None bound → bind OtelPublisher directly
|
|
215
|
+
- CompositePublisher → add OtelPublisher to it
|
|
216
|
+
- Single publisher → wrap both in a new CompositePublisher
|
|
217
|
+
"""
|
|
218
|
+
try:
|
|
219
|
+
from tenxgraph.runtime.publisher.base_publisher import BasePublisher
|
|
220
|
+
from tenxgraph.runtime.publisher.composite_publisher import CompositePublisher
|
|
221
|
+
from tenxgraph.runtime.publisher.otel_publisher import ObservabilityLevel, OtelPublisher
|
|
222
|
+
except ImportError:
|
|
223
|
+
logger.warning(
|
|
224
|
+
"OTEL_ENABLED=true but opentelemetry packages are not installed. "
|
|
225
|
+
"Install with: pip install '10xgraph[otel]'"
|
|
226
|
+
)
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
level = ObservabilityLevel(settings.OTEL_LEVEL.lower())
|
|
231
|
+
except ValueError:
|
|
232
|
+
logger.warning(
|
|
233
|
+
"Invalid OTEL_LEVEL=%r, falling back to 'standard'. Valid: spans, standard, full",
|
|
234
|
+
settings.OTEL_LEVEL,
|
|
235
|
+
)
|
|
236
|
+
level = ObservabilityLevel.STANDARD
|
|
237
|
+
|
|
238
|
+
otel_publisher = OtelPublisher(level=level)
|
|
239
|
+
existing = container.try_get(BasePublisher)
|
|
240
|
+
|
|
241
|
+
if existing is None:
|
|
242
|
+
container.bind_instance(BasePublisher, otel_publisher)
|
|
243
|
+
logger.info("OTEL: OtelPublisher bound (level=%s)", level)
|
|
244
|
+
elif isinstance(existing, CompositePublisher):
|
|
245
|
+
existing.add_publisher(otel_publisher)
|
|
246
|
+
logger.info("OTEL: OtelPublisher added to existing CompositePublisher (level=%s)", level)
|
|
247
|
+
else:
|
|
248
|
+
container.bind_instance(BasePublisher, CompositePublisher([existing, otel_publisher]))
|
|
249
|
+
logger.info(
|
|
250
|
+
"OTEL: wrapped %s + OtelPublisher in CompositePublisher (level=%s)",
|
|
251
|
+
type(existing).__name__,
|
|
252
|
+
level,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _setup_observability(container: InjectQ | None, graph_config: GraphConfig | None) -> None:
|
|
257
|
+
"""Wire Logfire/LangSmith from the ``observability`` block of ``10xgraph.json``.
|
|
258
|
+
|
|
259
|
+
Configures the tracer provider(s)/exporters and binds an ``OtelPublisher``
|
|
260
|
+
into the DI container. This runs at app-construction time, before the graph
|
|
261
|
+
is loaded and compiled — the only reliable attach point, because InjectQ
|
|
262
|
+
freezes the ``BasePublisher`` binding at ``container.compile()`` (which runs
|
|
263
|
+
inside ``StateGraph.compile()``). The core ``StateGraph.compile()`` guard
|
|
264
|
+
preserves a publisher already present in the container instead of clobbering
|
|
265
|
+
it with ``None``.
|
|
266
|
+
|
|
267
|
+
Secrets stay in the environment (``LOGFIRE_TOKEN``, ``LANGSMITH_API_KEY``);
|
|
268
|
+
only non-secret settings live in ``10xgraph.json``.
|
|
269
|
+
"""
|
|
270
|
+
if container is None or graph_config is None:
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
obs_cfg = graph_config.observability
|
|
274
|
+
if not obs_cfg:
|
|
275
|
+
return
|
|
276
|
+
|
|
277
|
+
logfire_on = bool((obs_cfg.get("logfire") or {}).get("enabled", False))
|
|
278
|
+
langsmith_on = bool((obs_cfg.get("langsmith") or {}).get("enabled", False))
|
|
279
|
+
if not logfire_on and not langsmith_on:
|
|
280
|
+
return
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
from tenxgraph.runtime.publisher.base_publisher import BasePublisher
|
|
284
|
+
from tenxgraph.runtime.publisher.composite_publisher import CompositePublisher
|
|
285
|
+
from tenxgraph.runtime.publisher.exporters import setup_observability
|
|
286
|
+
from tenxgraph.runtime.publisher.otel_publisher import ObservabilityLevel, OtelPublisher
|
|
287
|
+
except ImportError:
|
|
288
|
+
logger.warning(
|
|
289
|
+
"observability is configured but required packages are not installed. "
|
|
290
|
+
"Install with: pip install '10xgraph[observability]'"
|
|
291
|
+
)
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
# Configure the provider(s)/exporter(s) only. graph=None means no publisher
|
|
295
|
+
# is attached here; we bind it into the container ourselves below so it is
|
|
296
|
+
# in place before the graph compiles.
|
|
297
|
+
try:
|
|
298
|
+
setup_observability(None, obs_cfg)
|
|
299
|
+
except (ImportError, ValueError) as exc:
|
|
300
|
+
logger.warning("Observability setup skipped: %s", exc)
|
|
301
|
+
return
|
|
302
|
+
|
|
303
|
+
try:
|
|
304
|
+
level = ObservabilityLevel(str(obs_cfg.get("level", "standard")).lower())
|
|
305
|
+
except ValueError:
|
|
306
|
+
logger.warning(
|
|
307
|
+
"Invalid observability level=%r, falling back to 'standard'.",
|
|
308
|
+
obs_cfg.get("level"),
|
|
309
|
+
)
|
|
310
|
+
level = ObservabilityLevel.STANDARD
|
|
311
|
+
|
|
312
|
+
otel_publisher = OtelPublisher(level=level)
|
|
313
|
+
existing = container.try_get(BasePublisher)
|
|
314
|
+
|
|
315
|
+
if existing is None:
|
|
316
|
+
container.bind_instance(BasePublisher, otel_publisher)
|
|
317
|
+
elif isinstance(existing, CompositePublisher):
|
|
318
|
+
existing.add_publisher(otel_publisher)
|
|
319
|
+
else:
|
|
320
|
+
container.bind_instance(BasePublisher, CompositePublisher([existing, otel_publisher]))
|
|
321
|
+
|
|
322
|
+
logger.info(
|
|
323
|
+
"Observability enabled (logfire=%s, langsmith=%s, level=%s)",
|
|
324
|
+
logfire_on,
|
|
325
|
+
langsmith_on,
|
|
326
|
+
level,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _setup_otel(app: FastAPI, settings) -> None:
|
|
331
|
+
"""Configure OpenTelemetry tracing and instrument the FastAPI app.
|
|
332
|
+
|
|
333
|
+
Reads OTEL_EXPORTER_OTLP_ENDPOINT from settings. If unset, falls back to
|
|
334
|
+
the standard OTEL_EXPORTER_OTLP_ENDPOINT env var (honoured automatically
|
|
335
|
+
by the OTLP exporter). A ConsoleSpanExporter is used when no endpoint is
|
|
336
|
+
configured and the app is not in production, so traces are visible locally
|
|
337
|
+
without any collector.
|
|
338
|
+
"""
|
|
339
|
+
try:
|
|
340
|
+
from opentelemetry import trace
|
|
341
|
+
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
|
342
|
+
from opentelemetry.sdk.resources import Resource
|
|
343
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
344
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
345
|
+
except ImportError:
|
|
346
|
+
logger.warning(
|
|
347
|
+
"OTEL_ENABLED=true but opentelemetry packages are not installed. "
|
|
348
|
+
"Install with: pip install '10xgraph-api[otel]'"
|
|
349
|
+
)
|
|
350
|
+
return
|
|
351
|
+
|
|
352
|
+
resource = Resource.create({"service.name": settings.OTEL_SERVICE_NAME})
|
|
353
|
+
provider = TracerProvider(resource=resource)
|
|
354
|
+
|
|
355
|
+
endpoint = settings.OTEL_EXPORTER_OTLP_ENDPOINT
|
|
356
|
+
if endpoint:
|
|
357
|
+
try:
|
|
358
|
+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
|
359
|
+
|
|
360
|
+
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint=endpoint)))
|
|
361
|
+
logger.info("OTEL: exporting traces to %s", endpoint)
|
|
362
|
+
except ImportError:
|
|
363
|
+
logger.warning(
|
|
364
|
+
"OTEL_EXPORTER_OTLP_ENDPOINT is set but opentelemetry-exporter-otlp "
|
|
365
|
+
"is not installed. Install with: pip install '10xgraph-api[otel]'"
|
|
366
|
+
)
|
|
367
|
+
return
|
|
368
|
+
else:
|
|
369
|
+
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor
|
|
370
|
+
|
|
371
|
+
provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
|
|
372
|
+
logger.info("OTEL: no endpoint configured, printing spans to console")
|
|
373
|
+
|
|
374
|
+
trace.set_tracer_provider(provider)
|
|
375
|
+
FastAPIInstrumentor.instrument_app(app)
|
|
376
|
+
logger.info("OTEL: FastAPI instrumentation active (service=%s)", settings.OTEL_SERVICE_NAME)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def setup_middleware(
|
|
380
|
+
app: FastAPI,
|
|
381
|
+
graph_config: GraphConfig | None = None,
|
|
382
|
+
container: InjectQ | None = None,
|
|
383
|
+
):
|
|
384
|
+
"""
|
|
385
|
+
Set up middleware for the FastAPI application.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
app (FastAPI): The FastAPI application instance.
|
|
389
|
+
graph_config (GraphConfig | None): Optional graph configuration used to
|
|
390
|
+
enable dynamic rate limiting from ``10xgraph.json``.
|
|
391
|
+
|
|
392
|
+
Middleware:
|
|
393
|
+
- CORS: Configured based on settings.ORIGINS.
|
|
394
|
+
- TrustedHost: Configured with allowed hosts from settings.ALLOWED_HOST.
|
|
395
|
+
- GZip: Applied with a minimum size of 1000 bytes (excludes streaming endpoints).
|
|
396
|
+
- RateLimit: Applied when ``rate_limit`` is configured in ``10xgraph.json``.
|
|
397
|
+
"""
|
|
398
|
+
settings = get_settings()
|
|
399
|
+
|
|
400
|
+
# A concurrent run on the same thread is a conflict (409), not a 500.
|
|
401
|
+
if StaleStateError is not None:
|
|
402
|
+
app.add_exception_handler(StaleStateError, _stale_state_handler)
|
|
403
|
+
|
|
404
|
+
if settings.OTEL_ENABLED:
|
|
405
|
+
_setup_otel(app, settings)
|
|
406
|
+
if container is not None:
|
|
407
|
+
_attach_otel_publisher(container, settings)
|
|
408
|
+
|
|
409
|
+
# Declarative Logfire/LangSmith wiring from 10xgraph.json (before compile).
|
|
410
|
+
_setup_observability(container, graph_config)
|
|
411
|
+
|
|
412
|
+
# init cors
|
|
413
|
+
origins, allow_credentials = _resolve_cors_policy(settings)
|
|
414
|
+
app.add_middleware(
|
|
415
|
+
CORSMiddleware,
|
|
416
|
+
allow_origins=origins,
|
|
417
|
+
allow_credentials=allow_credentials,
|
|
418
|
+
allow_methods=["*"],
|
|
419
|
+
allow_headers=["*"],
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
app.add_middleware(
|
|
423
|
+
HealthCheckAwareTrustedHostMiddleware,
|
|
424
|
+
allowed_hosts=settings.ALLOWED_HOST.split(","),
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
# Add request size limit middleware (protects against DoS via large payloads). The upload
|
|
428
|
+
# route gets room for MEDIA_MAX_SIZE_MB, which the route itself enforces on the file.
|
|
429
|
+
app.add_middleware(
|
|
430
|
+
RequestSizeLimitMiddleware,
|
|
431
|
+
max_size=settings.MAX_REQUEST_SIZE,
|
|
432
|
+
path_limits=_upload_path_limits(settings.MAX_REQUEST_SIZE),
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
# Add security headers middleware (if enabled)
|
|
436
|
+
if settings.SECURITY_HEADERS_ENABLED:
|
|
437
|
+
app.add_middleware(
|
|
438
|
+
SecurityHeadersMiddleware,
|
|
439
|
+
enable_hsts=settings.HSTS_ENABLED,
|
|
440
|
+
hsts_max_age=settings.HSTS_MAX_AGE,
|
|
441
|
+
hsts_include_subdomains=settings.HSTS_INCLUDE_SUBDOMAINS,
|
|
442
|
+
hsts_preload=settings.HSTS_PRELOAD,
|
|
443
|
+
frame_options=settings.FRAME_OPTIONS,
|
|
444
|
+
content_type_options=settings.CONTENT_TYPE_OPTIONS,
|
|
445
|
+
xss_protection=settings.XSS_PROTECTION,
|
|
446
|
+
referrer_policy=settings.REFERRER_POLICY,
|
|
447
|
+
permissions_policy=settings.PERMISSIONS_POLICY,
|
|
448
|
+
csp_policy=settings.CSP_POLICY,
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
app.add_middleware(RequestIDMiddleware)
|
|
452
|
+
|
|
453
|
+
# Use SelectiveGZipMiddleware to exclude streaming endpoints from compression
|
|
454
|
+
# Streaming endpoints need immediate data transmission without buffering
|
|
455
|
+
app.add_middleware(SelectiveGZipMiddleware, minimum_size=1000)
|
|
456
|
+
|
|
457
|
+
# Apply rate limiting only when configured in 10xgraph.json
|
|
458
|
+
if graph_config is not None:
|
|
459
|
+
rate_limit_config = graph_config.rate_limit
|
|
460
|
+
if rate_limit_config is not None:
|
|
461
|
+
backend = build_backend(rate_limit_config, container=container)
|
|
462
|
+
# Store on app.state so lifespan can close it cleanly
|
|
463
|
+
app.state.rate_limit_backend = backend
|
|
464
|
+
app.add_middleware(
|
|
465
|
+
RateLimitMiddleware,
|
|
466
|
+
config=rate_limit_config,
|
|
467
|
+
backend=backend,
|
|
468
|
+
)
|
|
469
|
+
logger.info(
|
|
470
|
+
"Rate limiting enabled: backend=%s, %d req/%ds, by=%s, "
|
|
471
|
+
"exclude_paths=%s, trusted_proxy_headers=%s",
|
|
472
|
+
rate_limit_config.backend,
|
|
473
|
+
rate_limit_config.requests,
|
|
474
|
+
rate_limit_config.window,
|
|
475
|
+
rate_limit_config.by,
|
|
476
|
+
rate_limit_config.exclude_paths or "(none)",
|
|
477
|
+
rate_limit_config.trusted_proxy_headers,
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
logger.debug("Middleware set up")
|
|
481
|
+
|
|
482
|
+
# Initialize Sentry
|
|
483
|
+
init_sentry(settings)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# from typing import Any
|
|
2
|
+
|
|
3
|
+
# from taskiq import TaskiqMessage, TaskiqMiddleware, TaskiqResult
|
|
4
|
+
|
|
5
|
+
# from tenxgraph_api.src.app.core import logger
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# class MonitoringMiddleware(TaskiqMiddleware):
|
|
9
|
+
# """
|
|
10
|
+
# Middleware for monitoring task execution in Taskiq.
|
|
11
|
+
|
|
12
|
+
# This middleware logs various stages of task execution, including startup,
|
|
13
|
+
# shutdown, pre-execution, post-save, and error handling.
|
|
14
|
+
|
|
15
|
+
# Methods:
|
|
16
|
+
# startup: Logs a debug message when the middleware starts up.
|
|
17
|
+
# shutdown: Logs a debug message when the middleware shuts down.
|
|
18
|
+
# pre_execute: Logs an info message before a task is executed.
|
|
19
|
+
# post_save: Logs info and debug messages after a task is saved.
|
|
20
|
+
# on_error: Logs error messages when an exception occurs during task execution.
|
|
21
|
+
# """
|
|
22
|
+
|
|
23
|
+
# def startup(self) -> None:
|
|
24
|
+
# """
|
|
25
|
+
# Perform startup operations.
|
|
26
|
+
|
|
27
|
+
# This method logs a debug message indicating that the startup process is running.
|
|
28
|
+
|
|
29
|
+
# Returns:
|
|
30
|
+
# None
|
|
31
|
+
# """
|
|
32
|
+
# logger.debug("RUNNING STARTUP")
|
|
33
|
+
|
|
34
|
+
# def shutdown(self) -> None:
|
|
35
|
+
# """
|
|
36
|
+
# Perform shutdown operations.
|
|
37
|
+
|
|
38
|
+
# This method logs a debug message indicating that the shutdown process is running.
|
|
39
|
+
|
|
40
|
+
# Returns:
|
|
41
|
+
# None
|
|
42
|
+
# """
|
|
43
|
+
# logger.debug("RUNNING SHUTDOWN")
|
|
44
|
+
|
|
45
|
+
# def pre_execute(self, message: "TaskiqMessage") -> TaskiqMessage:
|
|
46
|
+
# """
|
|
47
|
+
# Pre-execution hook for processing a TaskiqMessage.
|
|
48
|
+
|
|
49
|
+
# This method is called before the task execution begins. It logs the task ID
|
|
50
|
+
# of the incoming message and returns the message unchanged.
|
|
51
|
+
|
|
52
|
+
# Args:
|
|
53
|
+
# message (TaskiqMessage): The message to be processed.
|
|
54
|
+
|
|
55
|
+
# Returns:
|
|
56
|
+
# TaskiqMessage: The same message that was passed in.
|
|
57
|
+
# """
|
|
58
|
+
# logger.info(f"PRE EXECUTE: {message.task_id}")
|
|
59
|
+
# return message
|
|
60
|
+
|
|
61
|
+
# def post_save(self, message: "TaskiqMessage", result: "TaskiqResult[Any]") -> None:
|
|
62
|
+
# """
|
|
63
|
+
# Handles post-save operations for a task.
|
|
64
|
+
|
|
65
|
+
# This method is called after a task is saved. It logs the task ID and the result of the
|
|
66
|
+
# task.
|
|
67
|
+
|
|
68
|
+
# Args:
|
|
69
|
+
# message (TaskiqMessage): The message object containing task details.
|
|
70
|
+
# result (TaskiqResult[Any]): The result object containing the outcome of the task.
|
|
71
|
+
|
|
72
|
+
# Returns:
|
|
73
|
+
# None
|
|
74
|
+
# """
|
|
75
|
+
# logger.info(f"Saved Task: {message.task_id}")
|
|
76
|
+
# logger.debug(f"Result: {result.return_value}")
|
|
77
|
+
|
|
78
|
+
# def on_error(
|
|
79
|
+
# self,
|
|
80
|
+
# message: "TaskiqMessage",
|
|
81
|
+
# result: "TaskiqResult[Any]",
|
|
82
|
+
# exception: BaseException,
|
|
83
|
+
# ) -> None:
|
|
84
|
+
# """
|
|
85
|
+
# Handles errors that occur during task execution.
|
|
86
|
+
|
|
87
|
+
# Args:
|
|
88
|
+
# message (TaskiqMessage): The message associated with the task.
|
|
89
|
+
# result (TaskiqResult[Any]): The result of the task execution.
|
|
90
|
+
# exception (BaseException): The exception that was raised.
|
|
91
|
+
|
|
92
|
+
# Returns:
|
|
93
|
+
# None
|
|
94
|
+
# """
|
|
95
|
+
# logger.error(f"Exception on task: {message.task_id}", exc_info=exception)
|
|
96
|
+
# logger.error(
|
|
97
|
+
# f"Exception on task {message.task_id} Result: {result.return_value if result
|
|
98
|
+
# else None}"
|
|
99
|
+
# )
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .general_exception import GeneralException
|
|
2
|
+
from .resources_exceptions import (
|
|
3
|
+
InvalidOperationError,
|
|
4
|
+
ResourceDuplicationError,
|
|
5
|
+
ResourceNotFoundError,
|
|
6
|
+
)
|
|
7
|
+
from .user_exception import UserAccountError, UserPermissionError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"GeneralException",
|
|
12
|
+
"InvalidOperationError",
|
|
13
|
+
"ResourceDuplicationError",
|
|
14
|
+
"ResourceNotFoundError",
|
|
15
|
+
"UserAccountError",
|
|
16
|
+
"UserPermissionError",
|
|
17
|
+
]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from tenxgraph_api.src.app.utils.schemas import ErrorSchemas
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class GeneralException(Exception):
|
|
5
|
+
"""
|
|
6
|
+
A custom exception class to handle general application errors.
|
|
7
|
+
|
|
8
|
+
Attributes:
|
|
9
|
+
message (str): Description of the error.
|
|
10
|
+
status_code (int): HTTP status code associated with the error.
|
|
11
|
+
error_code (str): Application-specific error code.
|
|
12
|
+
details (list[ErrorSchemas]): Optional list of detailed error schemas.
|
|
13
|
+
|
|
14
|
+
Methods:
|
|
15
|
+
__str__(): Returns a string representation of the exception.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
message="An error occurred",
|
|
21
|
+
status_code=400,
|
|
22
|
+
error_code="APP_ERROR",
|
|
23
|
+
details: list[ErrorSchemas] | None = None,
|
|
24
|
+
):
|
|
25
|
+
"""
|
|
26
|
+
Initializes a GeneralException instance.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
message (str, optional):
|
|
30
|
+
Description of the error. Defaults to "An error occurred".
|
|
31
|
+
status_code (int, optional):
|
|
32
|
+
HTTP status code associated with the error. Defaults to 400.
|
|
33
|
+
error_code (str, optional):
|
|
34
|
+
Application-specific error code. Defaults to "APP_ERROR".
|
|
35
|
+
details (list[ErrorSchemas], optional):
|
|
36
|
+
Optional list of detailed error schemas.
|
|
37
|
+
"""
|
|
38
|
+
self.message = message
|
|
39
|
+
self.status_code = status_code
|
|
40
|
+
self.error_code = error_code
|
|
41
|
+
self.details = details
|
|
42
|
+
super().__init__(self.message)
|
|
43
|
+
|
|
44
|
+
def __str__(self) -> str:
|
|
45
|
+
"""
|
|
46
|
+
Returns the string representation of the GeneralException instance.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
str: A string containing the message, status code, and error code.
|
|
50
|
+
"""
|
|
51
|
+
return (
|
|
52
|
+
"message: "
|
|
53
|
+
+ self.message
|
|
54
|
+
+ " status_code: "
|
|
55
|
+
+ str(self.status_code)
|
|
56
|
+
+ " error_code: "
|
|
57
|
+
+ self.error_code
|
|
58
|
+
)
|