aeonic-agentguard-sdk-python 0.1.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.
- aeonic/__init__.py +5 -0
- aeonic/adapters/__init__.py +1 -0
- aeonic/adapters/django.py +275 -0
- aeonic/adapters/fastapi.py +337 -0
- aeonic/core/__init__.py +4 -0
- aeonic/core/agent_registry.py +209 -0
- aeonic/core/agent_status_cache.py +28 -0
- aeonic/core/agentguard/__init__.py +8 -0
- aeonic/core/agentguard/registration.py +93 -0
- aeonic/core/agentguard/sse_client.py +210 -0
- aeonic/core/assessment/__init__.py +5 -0
- aeonic/core/assessment/executor.py +207 -0
- aeonic/core/buffer.py +27 -0
- aeonic/core/init.py +218 -0
- aeonic/core/introspection.py +468 -0
- aeonic/core/sender.py +234 -0
- aeonic/core/types.py +51 -0
- aeonic/utils/__init__.py +1 -0
- aeonic/utils/safe_serialize.py +22 -0
- aeonic_agentguard_sdk_python-0.1.0.dist-info/METADATA +770 -0
- aeonic_agentguard_sdk_python-0.1.0.dist-info/RECORD +24 -0
- aeonic_agentguard_sdk_python-0.1.0.dist-info/WHEEL +5 -0
- aeonic_agentguard_sdk_python-0.1.0.dist-info/licenses/LICENSE +21 -0
- aeonic_agentguard_sdk_python-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Route Introspection - Discover and register agent routes at SDK initialization.
|
|
3
|
+
|
|
4
|
+
This module implements deferred route introspection for FastAPI and Django,
|
|
5
|
+
allowing the SDK to discover all agent routes before any requests are made.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
from aeonic.core.types import AgentContext
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("aeonic")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def introspect_routes(app: Any) -> None:
|
|
17
|
+
"""
|
|
18
|
+
Introspect routes from FastAPI or Django app and register agents.
|
|
19
|
+
|
|
20
|
+
This function:
|
|
21
|
+
- Detects the framework automatically
|
|
22
|
+
- Walks the route/URL pattern registry
|
|
23
|
+
- Matches routes to agent metadata
|
|
24
|
+
- Registers all discovered agents
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
app: FastAPI or Django app instance (None for Django auto-detection)
|
|
28
|
+
"""
|
|
29
|
+
try:
|
|
30
|
+
framework = _detect_framework(app)
|
|
31
|
+
|
|
32
|
+
if framework == "fastapi":
|
|
33
|
+
if app is None:
|
|
34
|
+
logger.warning("[Aeonic] FastAPI introspection requires app instance")
|
|
35
|
+
return
|
|
36
|
+
_introspect_fastapi_routes(app)
|
|
37
|
+
elif framework == "django":
|
|
38
|
+
# Django can introspect even without app (uses get_resolver())
|
|
39
|
+
_introspect_django_routes(app)
|
|
40
|
+
else:
|
|
41
|
+
# Try Django as fallback if app is None
|
|
42
|
+
if app is None:
|
|
43
|
+
try:
|
|
44
|
+
import django # noqa: F401
|
|
45
|
+
|
|
46
|
+
_introspect_django_routes(None)
|
|
47
|
+
except ImportError:
|
|
48
|
+
logger.warning("[Aeonic] Unknown framework - cannot introspect routes")
|
|
49
|
+
else:
|
|
50
|
+
logger.warning("[Aeonic] Unknown framework - cannot introspect routes")
|
|
51
|
+
|
|
52
|
+
except Exception as e:
|
|
53
|
+
logger.warning(f"[Aeonic] Route introspection failed: {e}")
|
|
54
|
+
import traceback
|
|
55
|
+
|
|
56
|
+
logger.error(f"[Aeonic] Traceback: {traceback.format_exc()}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _detect_framework(app: Any) -> Optional[str]:
|
|
60
|
+
"""
|
|
61
|
+
Detect framework type from app instance.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
app: Application instance
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
"fastapi", "django", or None if unknown
|
|
68
|
+
"""
|
|
69
|
+
# FastAPI detection
|
|
70
|
+
if hasattr(app, "routes") or hasattr(app, "router"):
|
|
71
|
+
return "fastapi"
|
|
72
|
+
|
|
73
|
+
# Django detection
|
|
74
|
+
if hasattr(app, "url_patterns") or hasattr(app, "urlpatterns"):
|
|
75
|
+
return "django"
|
|
76
|
+
|
|
77
|
+
# Try to detect Django by checking if it's a module with get_resolver
|
|
78
|
+
try:
|
|
79
|
+
from django.urls import get_resolver # noqa: F401
|
|
80
|
+
|
|
81
|
+
# If app is a Django URLConf module
|
|
82
|
+
if hasattr(app, "__name__"):
|
|
83
|
+
return "django"
|
|
84
|
+
except ImportError:
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _introspect_fastapi_routes(app: Any) -> None:
|
|
91
|
+
"""
|
|
92
|
+
Introspect FastAPI routes and register agents.
|
|
93
|
+
|
|
94
|
+
FastAPI stores routes in app.routes or app.router.routes.
|
|
95
|
+
Each route has: path, methods, endpoint (handler function), and dependencies.
|
|
96
|
+
|
|
97
|
+
For FastAPI, we need to check the route's dependencies list to find with_agent() calls.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
app: FastAPI app instance
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
from aeonic.adapters.fastapi import get_agent_metadata
|
|
104
|
+
from aeonic.core.agent_registry import register_agent
|
|
105
|
+
from aeonic.core.init import get_config
|
|
106
|
+
|
|
107
|
+
# Get routes
|
|
108
|
+
routes = getattr(app, "routes", None)
|
|
109
|
+
if routes is None and hasattr(app, "router"):
|
|
110
|
+
routes = getattr(app.router, "routes", [])
|
|
111
|
+
|
|
112
|
+
if not routes:
|
|
113
|
+
logger.warning("[Aeonic] No routes found - routes may not have been added yet.")
|
|
114
|
+
logger.warning(
|
|
115
|
+
"[Aeonic] ⚠️ Ensure routes are defined before init() runs, or increase introspection_delay_ms."
|
|
116
|
+
)
|
|
117
|
+
logger.info(
|
|
118
|
+
"[Aeonic] ℹ️ Agents will be tracked at runtime on first request (fallback mode)."
|
|
119
|
+
)
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
config = get_config()
|
|
123
|
+
service_name = config.get("service_name")
|
|
124
|
+
agent_count = 0
|
|
125
|
+
|
|
126
|
+
for route in routes:
|
|
127
|
+
# Skip non-APIRoute routes (like Mount, etc.)
|
|
128
|
+
if not hasattr(route, "endpoint"):
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
# Skip FastAPI auto-generated routes by checking route type
|
|
132
|
+
route_type = type(route).__name__
|
|
133
|
+
if route_type not in ["APIRoute", "APIRouter"]:
|
|
134
|
+
# Skip Mount, StaticFiles, etc.
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
# Get route path first - FastAPI routes have 'path' attribute
|
|
138
|
+
route_path = getattr(route, "path", None)
|
|
139
|
+
if route_path is None:
|
|
140
|
+
# Try alternative attribute names
|
|
141
|
+
route_path = getattr(route, "path_regex", None)
|
|
142
|
+
if route_path:
|
|
143
|
+
# Convert regex pattern to path (remove ^$ and regex syntax)
|
|
144
|
+
import re
|
|
145
|
+
|
|
146
|
+
route_path = re.sub(r"^\^|\$$", "", str(route_path))
|
|
147
|
+
route_path = re.sub(r"\\", "", route_path)
|
|
148
|
+
else:
|
|
149
|
+
# Try to get from path_operation
|
|
150
|
+
if hasattr(route, "path_operation"):
|
|
151
|
+
route_path = getattr(route.path_operation, "path", None)
|
|
152
|
+
if route_path is None:
|
|
153
|
+
route_path = ""
|
|
154
|
+
|
|
155
|
+
# Check if this route has dependencies
|
|
156
|
+
agent_context: Optional[AgentContext] = None
|
|
157
|
+
|
|
158
|
+
# Try to get dependencies from the route
|
|
159
|
+
if hasattr(route, "dependant") and hasattr(route.dependant, "dependencies"):
|
|
160
|
+
# Check each dependency for agent metadata
|
|
161
|
+
for i, dep in enumerate(route.dependant.dependencies):
|
|
162
|
+
if hasattr(dep, "call"):
|
|
163
|
+
agent_context = get_agent_metadata(dep.call)
|
|
164
|
+
if agent_context:
|
|
165
|
+
break
|
|
166
|
+
else:
|
|
167
|
+
# Fallback: try to find metadata by checking closure
|
|
168
|
+
# FastAPI might wrap the dependency, so function IDs don't match
|
|
169
|
+
if hasattr(dep.call, "__closure__") and dep.call.__closure__:
|
|
170
|
+
for cell_idx, cell in enumerate(dep.call.__closure__):
|
|
171
|
+
try:
|
|
172
|
+
value = cell.cell_contents
|
|
173
|
+
# Check if closure contains our agent context (with source key)
|
|
174
|
+
if isinstance(value, dict):
|
|
175
|
+
if value.get("source") == "manual":
|
|
176
|
+
agent_context = value # type: ignore[assignment]
|
|
177
|
+
break
|
|
178
|
+
# Also check for agent dict by keys (name, type, model)
|
|
179
|
+
# The closure might have the original agent dict without 'source'
|
|
180
|
+
if (
|
|
181
|
+
"name" in value
|
|
182
|
+
and "type" in value
|
|
183
|
+
and "model" in value
|
|
184
|
+
):
|
|
185
|
+
# This looks like an agent context - add source and use it
|
|
186
|
+
agent_context = {
|
|
187
|
+
**value,
|
|
188
|
+
"source": "manual",
|
|
189
|
+
} # type: ignore[assignment]
|
|
190
|
+
break
|
|
191
|
+
# Check if closure contains a function with our metadata
|
|
192
|
+
if callable(value) and hasattr(
|
|
193
|
+
value, "__aeonic_metadata__"
|
|
194
|
+
):
|
|
195
|
+
agent_context = getattr(value, "__aeonic_metadata__") # type: ignore[assignment]
|
|
196
|
+
break
|
|
197
|
+
except (ValueError, AttributeError) as e:
|
|
198
|
+
logger.error(
|
|
199
|
+
f"[Aeonic] Could not inspect closure cell {cell_idx}: {e}"
|
|
200
|
+
)
|
|
201
|
+
continue
|
|
202
|
+
if agent_context:
|
|
203
|
+
break
|
|
204
|
+
|
|
205
|
+
# Also try checking the endpoint directly (in case it's decorated)
|
|
206
|
+
if not agent_context:
|
|
207
|
+
agent_context = get_agent_metadata(route.endpoint)
|
|
208
|
+
# if not agent_context:
|
|
209
|
+
# logger.warning("[Aeonic] ✗ No metadata found in endpoint")
|
|
210
|
+
|
|
211
|
+
if not agent_context:
|
|
212
|
+
# No agent metadata found - skip this route
|
|
213
|
+
# Only routes with with_agent() should be registered
|
|
214
|
+
# logger.warning(
|
|
215
|
+
# f"[Aeonic] Skipping route {route_path} - no agent metadata found (route does not use with_agent())"
|
|
216
|
+
# )
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
# Filter out FastAPI auto-generated routes
|
|
220
|
+
if route_path in ["/openapi.json", "/docs", "/redoc", "/openapi.yaml"]:
|
|
221
|
+
logger.warning(f"[Aeonic] Skipping auto-generated FastAPI route: {route_path}")
|
|
222
|
+
continue
|
|
223
|
+
|
|
224
|
+
# Get route methods (FastAPI routes have a methods attribute)
|
|
225
|
+
methods = getattr(route, "methods", None)
|
|
226
|
+
if methods is None:
|
|
227
|
+
# Try to get from path_operation
|
|
228
|
+
if hasattr(route, "path_operation"):
|
|
229
|
+
methods = getattr(route.path_operation, "methods", None)
|
|
230
|
+
if methods is None:
|
|
231
|
+
methods = {"GET"} # Default
|
|
232
|
+
|
|
233
|
+
# Convert methods to list if it's a set
|
|
234
|
+
if isinstance(methods, set):
|
|
235
|
+
methods = list(methods)
|
|
236
|
+
elif not isinstance(methods, (list, tuple)):
|
|
237
|
+
methods = [methods] if methods else ["GET"]
|
|
238
|
+
|
|
239
|
+
# Register agent for each HTTP method
|
|
240
|
+
for method in methods:
|
|
241
|
+
# Convert to string and uppercase
|
|
242
|
+
method = str(method).upper()
|
|
243
|
+
|
|
244
|
+
# Skip HEAD and OPTIONS if they're auto-generated
|
|
245
|
+
if method in ["HEAD", "OPTIONS"]:
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
register_agent(
|
|
249
|
+
name=agent_context.get("name", ""),
|
|
250
|
+
agent_type=agent_context.get("type"),
|
|
251
|
+
model=agent_context.get("model"),
|
|
252
|
+
route_path=route_path,
|
|
253
|
+
http_method=method,
|
|
254
|
+
framework="fastapi",
|
|
255
|
+
service_name=service_name,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
agent_count += 1
|
|
259
|
+
|
|
260
|
+
if agent_count == 0:
|
|
261
|
+
logger.warning("[Aeonic] No agent routes found during introspection")
|
|
262
|
+
|
|
263
|
+
except Exception as e:
|
|
264
|
+
logger.warning(f"[Aeonic] FastAPI introspection failed: {e}")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _introspect_django_routes(app: Any) -> None:
|
|
268
|
+
"""
|
|
269
|
+
Introspect Django URL patterns and register agents.
|
|
270
|
+
|
|
271
|
+
Django stores URL patterns in urlpatterns, accessible via get_resolver().
|
|
272
|
+
We need to walk the pattern tree recursively to handle include() patterns.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
app: Django app instance (or URLConf module, or None for auto-detection)
|
|
276
|
+
"""
|
|
277
|
+
try:
|
|
278
|
+
from django.urls import get_resolver
|
|
279
|
+
|
|
280
|
+
from aeonic.adapters.django import get_agent_metadata
|
|
281
|
+
from aeonic.core.agent_registry import register_agent
|
|
282
|
+
from aeonic.core.init import get_config
|
|
283
|
+
|
|
284
|
+
resolver = get_resolver()
|
|
285
|
+
config = get_config()
|
|
286
|
+
service_name = config.get("service_name")
|
|
287
|
+
|
|
288
|
+
agent_count = [0] # Use list to allow mutation in nested function
|
|
289
|
+
|
|
290
|
+
def walk_url_patterns(patterns: Any, prefix: str = "") -> None:
|
|
291
|
+
"""Recursively walk URL patterns."""
|
|
292
|
+
for pattern in patterns:
|
|
293
|
+
try:
|
|
294
|
+
# Handle nested patterns (include())
|
|
295
|
+
if hasattr(pattern, "url_patterns"):
|
|
296
|
+
# Get the pattern prefix
|
|
297
|
+
pattern_str = str(pattern.pattern) if hasattr(pattern, "pattern") else ""
|
|
298
|
+
walk_url_patterns(pattern.url_patterns, prefix + pattern_str)
|
|
299
|
+
else:
|
|
300
|
+
# Individual route
|
|
301
|
+
pattern_str = str(pattern.pattern) if hasattr(pattern, "pattern") else ""
|
|
302
|
+
path = prefix + pattern_str
|
|
303
|
+
|
|
304
|
+
# Get the view callback
|
|
305
|
+
if not hasattr(pattern, "callback") or pattern.callback is None:
|
|
306
|
+
continue
|
|
307
|
+
|
|
308
|
+
# Try to get agent metadata
|
|
309
|
+
agent_context = get_agent_metadata(pattern.callback)
|
|
310
|
+
if not agent_context:
|
|
311
|
+
continue
|
|
312
|
+
|
|
313
|
+
# Django doesn't store HTTP methods in URL patterns
|
|
314
|
+
# We need to check the view's allowed methods
|
|
315
|
+
methods = _get_view_methods(pattern.callback)
|
|
316
|
+
|
|
317
|
+
# If no methods found, skip this view (methods must be explicitly defined)
|
|
318
|
+
if not methods:
|
|
319
|
+
continue
|
|
320
|
+
|
|
321
|
+
for method in methods:
|
|
322
|
+
register_agent(
|
|
323
|
+
name=agent_context.get("name", ""),
|
|
324
|
+
agent_type=agent_context.get("type"),
|
|
325
|
+
model=agent_context.get("model"),
|
|
326
|
+
route_path=(f"/{path}" if not path.startswith("/") else path),
|
|
327
|
+
http_method=method,
|
|
328
|
+
framework="django",
|
|
329
|
+
service_name=service_name,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
agent_count[0] += 1
|
|
333
|
+
|
|
334
|
+
except Exception as e:
|
|
335
|
+
# Skip problematic patterns
|
|
336
|
+
logger.error(f"[Aeonic] Skipped pattern: {e}")
|
|
337
|
+
continue
|
|
338
|
+
|
|
339
|
+
# Start walking from root patterns
|
|
340
|
+
url_patterns = resolver.url_patterns
|
|
341
|
+
if not url_patterns:
|
|
342
|
+
logger.warning("[Aeonic] No URL patterns found")
|
|
343
|
+
return
|
|
344
|
+
|
|
345
|
+
walk_url_patterns(url_patterns)
|
|
346
|
+
|
|
347
|
+
if agent_count[0] == 0:
|
|
348
|
+
logger.warning("[Aeonic] No agent routes found during introspection")
|
|
349
|
+
|
|
350
|
+
except ImportError:
|
|
351
|
+
logger.error("[Aeonic] Django not installed - cannot introspect Django routes")
|
|
352
|
+
except Exception as e:
|
|
353
|
+
logger.error(f"[Aeonic] Django introspection failed: {e}")
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _get_view_methods(view_func: Any) -> list:
|
|
357
|
+
"""
|
|
358
|
+
Get HTTP methods supported by a Django view.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
view_func: Django view function or class
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
List of HTTP methods (e.g., ["GET", "POST"])
|
|
365
|
+
Returns empty list if methods cannot be determined (only registers explicitly defined methods)
|
|
366
|
+
"""
|
|
367
|
+
try:
|
|
368
|
+
# Function-based view - check if it has http_method_names attribute
|
|
369
|
+
# (This is set by decorators like @require_http_methods)
|
|
370
|
+
if hasattr(view_func, "http_method_names"):
|
|
371
|
+
methods = [
|
|
372
|
+
m.upper()
|
|
373
|
+
for m in view_func.http_method_names
|
|
374
|
+
if m.lower() not in ["head", "options"]
|
|
375
|
+
]
|
|
376
|
+
if methods:
|
|
377
|
+
return methods
|
|
378
|
+
|
|
379
|
+
# Class-based view - check the view class
|
|
380
|
+
if hasattr(view_func, "view_class"):
|
|
381
|
+
view_class = view_func.view_class
|
|
382
|
+
if hasattr(view_class, "http_method_names"):
|
|
383
|
+
methods = [
|
|
384
|
+
m.upper()
|
|
385
|
+
for m in view_class.http_method_names
|
|
386
|
+
if m.lower() not in ["head", "options"]
|
|
387
|
+
]
|
|
388
|
+
if methods:
|
|
389
|
+
return methods
|
|
390
|
+
|
|
391
|
+
# Check for as_view (CBV)
|
|
392
|
+
if hasattr(view_func, "cls"):
|
|
393
|
+
cls = view_func.cls
|
|
394
|
+
if hasattr(cls, "http_method_names"):
|
|
395
|
+
methods = [
|
|
396
|
+
m.upper() for m in cls.http_method_names if m.lower() not in ["head", "options"]
|
|
397
|
+
]
|
|
398
|
+
if methods:
|
|
399
|
+
return methods
|
|
400
|
+
|
|
401
|
+
# Try to check if view has method restrictions via decorators
|
|
402
|
+
# Check for @require_http_methods or similar decorators
|
|
403
|
+
if hasattr(view_func, "__wrapped__"):
|
|
404
|
+
# Check the wrapped function (might have method restrictions)
|
|
405
|
+
wrapped = view_func.__wrapped__
|
|
406
|
+
if hasattr(wrapped, "http_method_names"):
|
|
407
|
+
methods = [
|
|
408
|
+
m.upper()
|
|
409
|
+
for m in wrapped.http_method_names
|
|
410
|
+
if m.lower() not in ["head", "options"]
|
|
411
|
+
]
|
|
412
|
+
if methods:
|
|
413
|
+
return methods
|
|
414
|
+
|
|
415
|
+
# For function-based views without explicit method restrictions,
|
|
416
|
+
# try to infer methods from the view's code
|
|
417
|
+
# Check if view uses request.POST, request.body (POST) or request.GET (GET)
|
|
418
|
+
try:
|
|
419
|
+
import inspect
|
|
420
|
+
|
|
421
|
+
# Get the actual function (unwrap decorators)
|
|
422
|
+
actual_func = view_func
|
|
423
|
+
while hasattr(actual_func, "__wrapped__"):
|
|
424
|
+
actual_func = actual_func.__wrapped__
|
|
425
|
+
|
|
426
|
+
# Get source code
|
|
427
|
+
try:
|
|
428
|
+
source = inspect.getsource(actual_func)
|
|
429
|
+
methods = []
|
|
430
|
+
|
|
431
|
+
# Check for POST indicators
|
|
432
|
+
if (
|
|
433
|
+
"request.POST" in source
|
|
434
|
+
or "request.body" in source
|
|
435
|
+
or "request.method" in source
|
|
436
|
+
and "POST" in source
|
|
437
|
+
):
|
|
438
|
+
methods.append("POST")
|
|
439
|
+
|
|
440
|
+
# Check for GET indicators
|
|
441
|
+
if "request.GET" in source or (
|
|
442
|
+
"request.method" in source and "GET" in source and "POST" not in source
|
|
443
|
+
):
|
|
444
|
+
# Only add GET if POST wasn't found (to avoid both)
|
|
445
|
+
if "POST" not in methods:
|
|
446
|
+
methods.append("GET")
|
|
447
|
+
|
|
448
|
+
if methods:
|
|
449
|
+
return [m.upper() for m in methods]
|
|
450
|
+
except (OSError, TypeError):
|
|
451
|
+
# Can't get source code (might be C extension or other)
|
|
452
|
+
pass
|
|
453
|
+
except Exception:
|
|
454
|
+
pass
|
|
455
|
+
|
|
456
|
+
# If we still can't determine, return empty list
|
|
457
|
+
# Users should use @require_http_methods(['POST']) to explicitly define methods
|
|
458
|
+
logger.warning(
|
|
459
|
+
f"[Aeonic] Could not determine HTTP methods for view {getattr(view_func, '__name__', 'unknown')}. "
|
|
460
|
+
f"Use @require_http_methods(['POST']) decorator to explicitly define supported methods. "
|
|
461
|
+
f"Skipping method registration for this view."
|
|
462
|
+
)
|
|
463
|
+
return []
|
|
464
|
+
|
|
465
|
+
except Exception as e:
|
|
466
|
+
logger.error(f"[Aeonic] Error determining view methods: {e}")
|
|
467
|
+
# Return empty list - be conservative
|
|
468
|
+
return []
|