nat-engine 1__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.
- mannf/__init__.py +33 -0
- mannf/__main__.py +10 -0
- mannf/_version.py +8 -0
- mannf/agents/__init__.py +7 -0
- mannf/agents/analyzer_agent.py +9 -0
- mannf/agents/base.py +9 -0
- mannf/agents/bdi_agent.py +9 -0
- mannf/agents/belief_state.py +9 -0
- mannf/agents/coordinator_agent.py +9 -0
- mannf/agents/executor_agent.py +9 -0
- mannf/agents/monitor_agent.py +9 -0
- mannf/agents/oracle_agent.py +9 -0
- mannf/agents/planner_agent.py +9 -0
- mannf/agents/test_agent.py +9 -0
- mannf/anomaly/__init__.py +7 -0
- mannf/anomaly/enhanced_detector.py +9 -0
- mannf/cli.py +9 -0
- mannf/core/__init__.py +26 -0
- mannf/core/agents/__init__.py +52 -0
- mannf/core/agents/accessibility_scanner_agent.py +245 -0
- mannf/core/agents/analyzer_agent.py +224 -0
- mannf/core/agents/autonomous_loop_agent.py +1086 -0
- mannf/core/agents/autonomous_loop_models.py +62 -0
- mannf/core/agents/autonomous_run_differ.py +427 -0
- mannf/core/agents/base.py +128 -0
- mannf/core/agents/bdi_agent.py +330 -0
- mannf/core/agents/belief_state.py +202 -0
- mannf/core/agents/browser_coordinator_agent.py +224 -0
- mannf/core/agents/browser_executor_agent.py +410 -0
- mannf/core/agents/coordinator_agent.py +262 -0
- mannf/core/agents/executor_agent.py +222 -0
- mannf/core/agents/monitor_agent.py +188 -0
- mannf/core/agents/oracle_agent.py +150 -0
- mannf/core/agents/performance_testing_agent.py +279 -0
- mannf/core/agents/planner_agent.py +128 -0
- mannf/core/agents/test_agent.py +249 -0
- mannf/core/agents/visual_regression_agent.py +311 -0
- mannf/core/agents/web_crawler_agent.py +510 -0
- mannf/core/agents/worker_pool.py +366 -0
- mannf/core/anomaly/__init__.py +14 -0
- mannf/core/anomaly/enhanced_detector.py +541 -0
- mannf/core/browser/__init__.py +63 -0
- mannf/core/browser/accessibility_scanner.py +424 -0
- mannf/core/browser/discovery_model.py +178 -0
- mannf/core/browser/dom_snapshot.py +349 -0
- mannf/core/browser/ingestor_bridge.py +371 -0
- mannf/core/browser/performance_metrics.py +217 -0
- mannf/core/browser/reflection_analyzer.py +442 -0
- mannf/core/browser/scenario_generator.py +1100 -0
- mannf/core/browser/security_scenario_generator.py +695 -0
- mannf/core/browser/visual_comparer.py +159 -0
- mannf/core/diagnostics/__init__.py +28 -0
- mannf/core/diagnostics/failure_clusterer.py +211 -0
- mannf/core/diagnostics/flake_detector.py +233 -0
- mannf/core/diagnostics/root_cause_analyzer.py +273 -0
- mannf/core/distributed/__init__.py +16 -0
- mannf/core/distributed/endpoint.py +139 -0
- mannf/core/distributed/system_under_test.py +207 -0
- mannf/core/functional_orchestrator.py +428 -0
- mannf/core/messaging/__init__.py +11 -0
- mannf/core/messaging/bus.py +113 -0
- mannf/core/messaging/messages.py +89 -0
- mannf/core/nat_orchestrator.py +342 -0
- mannf/core/neural/__init__.py +183 -0
- mannf/core/orchestrator.py +272 -0
- mannf/core/prioritization/__init__.py +17 -0
- mannf/core/prioritization/adaptive_controller.py +509 -0
- mannf/core/prioritization/belief_prioritizer.py +231 -0
- mannf/core/prioritization/risk_scorer.py +430 -0
- mannf/core/reporting/__init__.py +12 -0
- mannf/core/reporting/unified_report.py +664 -0
- mannf/core/testing/__init__.py +17 -0
- mannf/core/testing/adaptive_controller.py +149 -0
- mannf/core/testing/models.py +179 -0
- mannf/core/validation/__init__.py +10 -0
- mannf/core/validation/self_validation_runner.py +180 -0
- mannf/dashboard/__init__.py +7 -0
- mannf/dashboard/app.py +9 -0
- mannf/dashboard/models.py +9 -0
- mannf/dashboard/static/index.html +2538 -0
- mannf/dashboard/telemetry.py +9 -0
- mannf/distributed/__init__.py +7 -0
- mannf/distributed/endpoint.py +9 -0
- mannf/distributed/system_under_test.py +9 -0
- mannf/healing/__init__.py +7 -0
- mannf/healing/graphql_schema_diff.py +9 -0
- mannf/healing/healer.py +9 -0
- mannf/healing/models.py +9 -0
- mannf/healing/schema_diff.py +9 -0
- mannf/integrations/__init__.py +7 -0
- mannf/integrations/auth.py +9 -0
- mannf/integrations/graphql_parser.py +9 -0
- mannf/integrations/graphql_sut.py +9 -0
- mannf/integrations/http_sut.py +9 -0
- mannf/integrations/openapi_parser.py +9 -0
- mannf/integrations/postman_parser.py +9 -0
- mannf/llm/__init__.py +7 -0
- mannf/llm/anthropic_provider.py +9 -0
- mannf/llm/base.py +9 -0
- mannf/llm/config.py +9 -0
- mannf/llm/factory.py +9 -0
- mannf/llm/openai_provider.py +9 -0
- mannf/llm/prompts.py +9 -0
- mannf/messaging/__init__.py +7 -0
- mannf/messaging/bus.py +9 -0
- mannf/messaging/messages.py +9 -0
- mannf/nat_orchestrator.py +9 -0
- mannf/neural/__init__.py +7 -0
- mannf/orchestrator.py +9 -0
- mannf/prioritization/__init__.py +7 -0
- mannf/prioritization/adaptive_controller.py +9 -0
- mannf/prioritization/belief_prioritizer.py +9 -0
- mannf/prioritization/risk_scorer.py +9 -0
- mannf/product/__init__.py +29 -0
- mannf/product/admin/__init__.py +3 -0
- mannf/product/admin/routes.py +514 -0
- mannf/product/auth/__init__.py +5 -0
- mannf/product/auth/saml.py +212 -0
- mannf/product/billing/__init__.py +5 -0
- mannf/product/billing/audit.py +160 -0
- mannf/product/billing/feature_gates.py +180 -0
- mannf/product/billing/metering.py +179 -0
- mannf/product/billing/notifications.py +181 -0
- mannf/product/billing/plans.py +133 -0
- mannf/product/billing/rate_limits.py +35 -0
- mannf/product/billing/stripe_billing.py +906 -0
- mannf/product/billing/tenant_auth.py +233 -0
- mannf/product/billing/tenant_manager.py +873 -0
- mannf/product/cli.py +3900 -0
- mannf/product/cli_admin.py +408 -0
- mannf/product/dashboard/__init__.py +61 -0
- mannf/product/dashboard/app.py +3567 -0
- mannf/product/dashboard/models.py +460 -0
- mannf/product/dashboard/static/index.html +6347 -0
- mannf/product/dashboard/static/manifest.json +25 -0
- mannf/product/dashboard/static/pwa-icon-192.png +0 -0
- mannf/product/dashboard/static/pwa-icon-512.png +0 -0
- mannf/product/dashboard/static/sw.js +64 -0
- mannf/product/dashboard/telemetry.py +547 -0
- mannf/product/database.py +145 -0
- mannf/product/demo.py +844 -0
- mannf/product/doctor.py +509 -0
- mannf/product/exporters/__init__.py +65 -0
- mannf/product/exporters/azuredevops_exporter.py +257 -0
- mannf/product/exporters/base.py +307 -0
- mannf/product/exporters/bugzilla_exporter.py +200 -0
- mannf/product/exporters/dedup.py +275 -0
- mannf/product/exporters/finding_adapter.py +216 -0
- mannf/product/exporters/github_exporter.py +197 -0
- mannf/product/exporters/gitlab_exporter.py +215 -0
- mannf/product/exporters/jira_exporter.py +180 -0
- mannf/product/exporters/linear_exporter.py +195 -0
- mannf/product/exporters/loader.py +233 -0
- mannf/product/exporters/pagerduty_exporter.py +363 -0
- mannf/product/exporters/sentry_exporter.py +322 -0
- mannf/product/exporters/servicenow_exporter.py +240 -0
- mannf/product/exporters/shortcut_exporter.py +231 -0
- mannf/product/exporters/webhook_exporter.py +383 -0
- mannf/product/formatters/__init__.py +18 -0
- mannf/product/formatters/allure_formatter.py +161 -0
- mannf/product/formatters/ctrf_formatter.py +149 -0
- mannf/product/healing/__init__.py +30 -0
- mannf/product/healing/graphql_schema_diff.py +152 -0
- mannf/product/healing/healer.py +141 -0
- mannf/product/healing/models.py +175 -0
- mannf/product/healing/schema_diff.py +251 -0
- mannf/product/ingestors/__init__.py +77 -0
- mannf/product/ingestors/base.py +256 -0
- mannf/product/ingestors/bgstm_ingestor.py +764 -0
- mannf/product/ingestors/curl_ingestor.py +1019 -0
- mannf/product/ingestors/cypress_ingestor.py +487 -0
- mannf/product/ingestors/gherkin_ingestor.py +967 -0
- mannf/product/ingestors/graphql_ingestor.py +845 -0
- mannf/product/ingestors/grpc_ingestor.py +591 -0
- mannf/product/ingestors/har_ingestor.py +976 -0
- mannf/product/ingestors/loader.py +284 -0
- mannf/product/ingestors/models.py +146 -0
- mannf/product/ingestors/openapi_ingestor.py +606 -0
- mannf/product/ingestors/playwright_ingestor.py +449 -0
- mannf/product/ingestors/postman_ingestor.py +631 -0
- mannf/product/ingestors/traffic_ingestor.py +679 -0
- mannf/product/ingestors/websocket_ingestor.py +526 -0
- mannf/product/integrations/__init__.py +21 -0
- mannf/product/integrations/auth.py +190 -0
- mannf/product/integrations/graphql_parser.py +436 -0
- mannf/product/integrations/graphql_sut.py +247 -0
- mannf/product/integrations/grpc_sut.py +469 -0
- mannf/product/integrations/http_sut.py +237 -0
- mannf/product/integrations/kafka_adapter.py +342 -0
- mannf/product/integrations/openapi_parser.py +513 -0
- mannf/product/integrations/postman_parser.py +467 -0
- mannf/product/integrations/webhook_receiver.py +344 -0
- mannf/product/integrations/websocket_sut.py +434 -0
- mannf/product/llm/__init__.py +25 -0
- mannf/product/llm/anthropic_provider.py +94 -0
- mannf/product/llm/base.py +267 -0
- mannf/product/llm/config.py +48 -0
- mannf/product/llm/factory.py +42 -0
- mannf/product/llm/openai_provider.py +93 -0
- mannf/product/llm/prompts.py +403 -0
- mannf/product/llm/root_cause_service.py +311 -0
- mannf/product/llm/test_plan_models.py +78 -0
- mannf/product/metrics.py +149 -0
- mannf/product/middleware/__init__.py +3 -0
- mannf/product/middleware/audit_middleware.py +112 -0
- mannf/product/middleware/tenant_isolation.py +114 -0
- mannf/product/models.py +347 -0
- mannf/product/notifications/__init__.py +24 -0
- mannf/product/notifications/dispatcher.py +411 -0
- mannf/product/onboarding.py +190 -0
- mannf/product/orchestration/__init__.py +39 -0
- mannf/product/orchestration/ingest_scan_orchestrator.py +339 -0
- mannf/product/orchestration/pipeline.py +401 -0
- mannf/product/orchestrator.py +987 -0
- mannf/product/orchestrator_models.py +269 -0
- mannf/product/regression/__init__.py +36 -0
- mannf/product/regression/differ.py +172 -0
- mannf/product/regression/masking.py +100 -0
- mannf/product/regression/models.py +232 -0
- mannf/product/regression/recorder.py +124 -0
- mannf/product/regression/replayer.py +168 -0
- mannf/product/reports/__init__.py +10 -0
- mannf/product/reports/pdf.py +132 -0
- mannf/product/scheduling/__init__.py +57 -0
- mannf/product/scheduling/cron_utils.py +251 -0
- mannf/product/scheduling/engine.py +473 -0
- mannf/product/scheduling/models.py +86 -0
- mannf/product/scheduling/queue.py +894 -0
- mannf/product/scheduling/store.py +235 -0
- mannf/product/security/__init__.py +21 -0
- mannf/product/security/belief_guided.py +143 -0
- mannf/product/security/checks/__init__.py +55 -0
- mannf/product/security/checks/base.py +69 -0
- mannf/product/security/checks/bfla.py +77 -0
- mannf/product/security/checks/bola.py +77 -0
- mannf/product/security/checks/bopla.py +80 -0
- mannf/product/security/checks/broken_auth.py +86 -0
- mannf/product/security/checks/graphql_security.py +299 -0
- mannf/product/security/checks/inventory.py +70 -0
- mannf/product/security/checks/misconfig.py +158 -0
- mannf/product/security/checks/resource_consumption.py +70 -0
- mannf/product/security/checks/sensitive_flows.py +80 -0
- mannf/product/security/checks/ssrf.py +101 -0
- mannf/product/security/checks/unsafe_consumption.py +120 -0
- mannf/product/security/models.py +92 -0
- mannf/product/security/plugin_loader.py +182 -0
- mannf/product/security/reporter.py +92 -0
- mannf/product/security/scanner.py +183 -0
- mannf/product/server.py +6220 -0
- mannf/product/setup_wizard.py +873 -0
- mannf/product/status.py +404 -0
- mannf/product/storage/__init__.py +10 -0
- mannf/product/storage/artifact_store.py +343 -0
- mannf/product/telemetry.py +300 -0
- mannf/product/uninstall.py +169 -0
- mannf/product/upgrade.py +139 -0
- mannf/product/weights/__init__.py +13 -0
- mannf/product/weights/blob_store.py +299 -0
- mannf/product/weights/factory.py +42 -0
- mannf/product/weights/registry.py +159 -0
- mannf/product/weights/store.py +210 -0
- mannf/regression/__init__.py +7 -0
- mannf/regression/differ.py +9 -0
- mannf/regression/masking.py +9 -0
- mannf/regression/models.py +9 -0
- mannf/regression/recorder.py +9 -0
- mannf/regression/replayer.py +9 -0
- mannf/security/__init__.py +7 -0
- mannf/security/belief_guided.py +9 -0
- mannf/security/checks/__init__.py +7 -0
- mannf/security/checks/base.py +9 -0
- mannf/security/checks/bfla.py +9 -0
- mannf/security/checks/bola.py +9 -0
- mannf/security/checks/bopla.py +9 -0
- mannf/security/checks/broken_auth.py +9 -0
- mannf/security/checks/graphql_security.py +9 -0
- mannf/security/checks/inventory.py +9 -0
- mannf/security/checks/misconfig.py +9 -0
- mannf/security/checks/resource_consumption.py +9 -0
- mannf/security/checks/sensitive_flows.py +9 -0
- mannf/security/checks/ssrf.py +9 -0
- mannf/security/checks/unsafe_consumption.py +9 -0
- mannf/security/models.py +9 -0
- mannf/security/reporter.py +9 -0
- mannf/security/scanner.py +9 -0
- mannf/server.py +9 -0
- mannf/testing/__init__.py +7 -0
- mannf/testing/adaptive_controller.py +9 -0
- mannf/testing/models.py +9 -0
- mannf/weights/__init__.py +7 -0
- mannf/weights/registry.py +9 -0
- mannf/weights/store.py +9 -0
- nat_engine-1.dist-info/METADATA +555 -0
- nat_engine-1.dist-info/RECORD +299 -0
- nat_engine-1.dist-info/WHEEL +5 -0
- nat_engine-1.dist-info/entry_points.txt +4 -0
- nat_engine-1.dist-info/licenses/LICENSE +651 -0
- nat_engine-1.dist-info/licenses/NOTICE +178 -0
- nat_engine-1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# Copyright (C) 2026 Brad Guider
|
|
2
|
+
# This file is part of NAT (Neural Agent Testing Framework).
|
|
3
|
+
# Licensed under the AGPL-3.0. See LICENSE for details.
|
|
4
|
+
# Commercial licensing available — see COMMERCIAL_LICENSE.md.
|
|
5
|
+
|
|
6
|
+
"""Plugin loader for custom IngestorPlugin implementations.
|
|
7
|
+
|
|
8
|
+
Provides two discovery mechanisms:
|
|
9
|
+
- Directory scanning: load .py files from a directory path.
|
|
10
|
+
- Entry-point discovery: use importlib.metadata to find registered ingestors.
|
|
11
|
+
|
|
12
|
+
Both mechanisms validate that each discovered class has the required attributes
|
|
13
|
+
and callable methods before instantiating it. Invalid ingestors are skipped
|
|
14
|
+
with a warning — they will never cause the scanner to crash.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import importlib
|
|
20
|
+
import importlib.util
|
|
21
|
+
import inspect
|
|
22
|
+
import logging
|
|
23
|
+
import sys
|
|
24
|
+
from importlib.metadata import entry_points
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
from mannf.product.ingestors.base import IngestorPlugin
|
|
28
|
+
from mannf.product.ingestors.models import IngestorSource
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
_REQUIRED_ATTRS = ("name", "display_name", "description", "supported_extensions", "supported_formats")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _is_valid_ingestor(cls: type) -> bool:
|
|
36
|
+
"""Return True if *cls* is a valid, concrete IngestorPlugin subclass."""
|
|
37
|
+
if not (isinstance(cls, type) and issubclass(cls, IngestorPlugin) and cls is not IngestorPlugin):
|
|
38
|
+
return False
|
|
39
|
+
for attr in _REQUIRED_ATTRS:
|
|
40
|
+
if not hasattr(cls, attr):
|
|
41
|
+
logger.warning(
|
|
42
|
+
"Ingestor %s is missing required attribute '%s' — skipping.",
|
|
43
|
+
cls.__name__,
|
|
44
|
+
attr,
|
|
45
|
+
)
|
|
46
|
+
return False
|
|
47
|
+
if not callable(getattr(cls, "ingest", None)):
|
|
48
|
+
logger.warning(
|
|
49
|
+
"Ingestor %s has no callable 'ingest' method — skipping.",
|
|
50
|
+
cls.__name__,
|
|
51
|
+
)
|
|
52
|
+
return False
|
|
53
|
+
if not callable(getattr(cls, "validate_config", None)):
|
|
54
|
+
logger.warning(
|
|
55
|
+
"Ingestor %s has no callable 'validate_config' method — skipping.",
|
|
56
|
+
cls.__name__,
|
|
57
|
+
)
|
|
58
|
+
return False
|
|
59
|
+
if not callable(getattr(cls, "can_handle", None)):
|
|
60
|
+
logger.warning(
|
|
61
|
+
"Ingestor %s has no callable 'can_handle' method — skipping.",
|
|
62
|
+
cls.__name__,
|
|
63
|
+
)
|
|
64
|
+
return False
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _instantiate(cls: type) -> IngestorPlugin | None:
|
|
69
|
+
"""Try to instantiate *cls*; log a warning and return None on failure."""
|
|
70
|
+
try:
|
|
71
|
+
return cls()
|
|
72
|
+
except Exception as exc: # noqa: BLE001
|
|
73
|
+
logger.warning(
|
|
74
|
+
"Ingestor %s raised %s during __init__ — skipping.", cls.__name__, exc
|
|
75
|
+
)
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def load_ingestors_from_directory(directory: Path) -> list[IngestorPlugin]:
|
|
80
|
+
"""Discover and load IngestorPlugin subclasses from all ``.py`` files in *directory*.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
directory:
|
|
85
|
+
Path to a directory containing ingestor ``.py`` files.
|
|
86
|
+
|
|
87
|
+
Returns
|
|
88
|
+
-------
|
|
89
|
+
list[IngestorPlugin]
|
|
90
|
+
Instantiated ingestor objects for every valid ingestor found.
|
|
91
|
+
"""
|
|
92
|
+
ingestors: list[IngestorPlugin] = []
|
|
93
|
+
|
|
94
|
+
if not directory.exists():
|
|
95
|
+
logger.warning(
|
|
96
|
+
"Ingestor directory '%s' does not exist — no ingestors loaded.", directory
|
|
97
|
+
)
|
|
98
|
+
return ingestors
|
|
99
|
+
|
|
100
|
+
if not directory.is_dir():
|
|
101
|
+
logger.warning(
|
|
102
|
+
"Ingestor path '%s' is not a directory — no ingestors loaded.", directory
|
|
103
|
+
)
|
|
104
|
+
return ingestors
|
|
105
|
+
|
|
106
|
+
for py_file in sorted(directory.glob("*.py")):
|
|
107
|
+
module_name = f"_nat_ingestor_{py_file.stem}"
|
|
108
|
+
try:
|
|
109
|
+
spec = importlib.util.spec_from_file_location(module_name, py_file)
|
|
110
|
+
if spec is None or spec.loader is None:
|
|
111
|
+
logger.warning(
|
|
112
|
+
"Could not create module spec for '%s' — skipping.", py_file
|
|
113
|
+
)
|
|
114
|
+
continue
|
|
115
|
+
module = importlib.util.module_from_spec(spec)
|
|
116
|
+
sys.modules[module_name] = module
|
|
117
|
+
spec.loader.exec_module(module) # type: ignore[union-attr]
|
|
118
|
+
except Exception as exc: # noqa: BLE001
|
|
119
|
+
logger.warning(
|
|
120
|
+
"Failed to import ingestor file '%s': %s — skipping.", py_file, exc
|
|
121
|
+
)
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
for _name, obj in inspect.getmembers(module, inspect.isclass):
|
|
125
|
+
if obj.__module__ != module_name:
|
|
126
|
+
# Avoid re-processing imported base classes
|
|
127
|
+
continue
|
|
128
|
+
if _is_valid_ingestor(obj):
|
|
129
|
+
instance = _instantiate(obj)
|
|
130
|
+
if instance is not None:
|
|
131
|
+
ingestors.append(instance)
|
|
132
|
+
|
|
133
|
+
return ingestors
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def load_ingestors_from_entry_points(
|
|
137
|
+
group: str = "nat.ingestors",
|
|
138
|
+
) -> list[IngestorPlugin]:
|
|
139
|
+
"""Discover and load IngestorPlugin subclasses registered as package entry points.
|
|
140
|
+
|
|
141
|
+
Parameters
|
|
142
|
+
----------
|
|
143
|
+
group:
|
|
144
|
+
The entry-point group name to scan (default ``"nat.ingestors"``).
|
|
145
|
+
|
|
146
|
+
Returns
|
|
147
|
+
-------
|
|
148
|
+
list[IngestorPlugin]
|
|
149
|
+
Instantiated ingestor objects for every valid entry point found.
|
|
150
|
+
"""
|
|
151
|
+
ingestors: list[IngestorPlugin] = []
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
eps = entry_points(group=group)
|
|
155
|
+
except Exception as exc: # noqa: BLE001
|
|
156
|
+
logger.warning(
|
|
157
|
+
"Failed to query entry points for group '%s': %s", group, exc
|
|
158
|
+
)
|
|
159
|
+
return ingestors
|
|
160
|
+
|
|
161
|
+
for ep in eps:
|
|
162
|
+
try:
|
|
163
|
+
obj = ep.load()
|
|
164
|
+
except Exception as exc: # noqa: BLE001
|
|
165
|
+
logger.warning(
|
|
166
|
+
"Failed to load entry point '%s': %s — skipping.", ep.name, exc
|
|
167
|
+
)
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
if _is_valid_ingestor(obj):
|
|
171
|
+
instance = _instantiate(obj)
|
|
172
|
+
if instance is not None:
|
|
173
|
+
ingestors.append(instance)
|
|
174
|
+
else:
|
|
175
|
+
logger.warning(
|
|
176
|
+
"Entry point '%s' does not point to a valid IngestorPlugin subclass — skipping.",
|
|
177
|
+
ep.name,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
return ingestors
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def load_all_ingestors(
|
|
184
|
+
ingestors_dir: Path | None = None,
|
|
185
|
+
entry_point_group: str = "nat.ingestors",
|
|
186
|
+
) -> list[IngestorPlugin]:
|
|
187
|
+
"""Load ingestors from built-ins, a directory, and registered entry points.
|
|
188
|
+
|
|
189
|
+
Results are deduplicated by class name; built-in ingestors take precedence.
|
|
190
|
+
|
|
191
|
+
Parameters
|
|
192
|
+
----------
|
|
193
|
+
ingestors_dir:
|
|
194
|
+
Optional path to a directory of ingestor ``.py`` files.
|
|
195
|
+
entry_point_group:
|
|
196
|
+
Entry-point group to scan (default ``"nat.ingestors"``).
|
|
197
|
+
|
|
198
|
+
Returns
|
|
199
|
+
-------
|
|
200
|
+
list[IngestorPlugin]
|
|
201
|
+
Merged, deduplicated list of instantiated ingestor objects.
|
|
202
|
+
"""
|
|
203
|
+
from mannf.product.ingestors import BUILTIN_INGESTORS # avoid circular import
|
|
204
|
+
|
|
205
|
+
seen_class_names: set[str] = set()
|
|
206
|
+
combined: list[IngestorPlugin] = []
|
|
207
|
+
|
|
208
|
+
dir_ingestors = (
|
|
209
|
+
load_ingestors_from_directory(ingestors_dir) if ingestors_dir else []
|
|
210
|
+
)
|
|
211
|
+
ep_ingestors = load_ingestors_from_entry_points(entry_point_group)
|
|
212
|
+
|
|
213
|
+
for ingestor in BUILTIN_INGESTORS + dir_ingestors + ep_ingestors:
|
|
214
|
+
class_name = type(ingestor).__name__
|
|
215
|
+
if class_name not in seen_class_names:
|
|
216
|
+
seen_class_names.add(class_name)
|
|
217
|
+
combined.append(ingestor)
|
|
218
|
+
|
|
219
|
+
return combined
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def get_ingestor_by_name(
|
|
223
|
+
name: str,
|
|
224
|
+
ingestors: list[IngestorPlugin] | None = None,
|
|
225
|
+
) -> IngestorPlugin | None:
|
|
226
|
+
"""Look up an ingestor by its ``name`` attribute.
|
|
227
|
+
|
|
228
|
+
Parameters
|
|
229
|
+
----------
|
|
230
|
+
name:
|
|
231
|
+
The ingestor name to search for (e.g., ``"openapi"``, ``"postman"``).
|
|
232
|
+
ingestors:
|
|
233
|
+
List of instantiated ingestor objects to search. Defaults to
|
|
234
|
+
:func:`load_all_ingestors` if not provided.
|
|
235
|
+
|
|
236
|
+
Returns
|
|
237
|
+
-------
|
|
238
|
+
IngestorPlugin | None
|
|
239
|
+
The first matching ingestor, or ``None`` if not found.
|
|
240
|
+
"""
|
|
241
|
+
if ingestors is None:
|
|
242
|
+
ingestors = load_all_ingestors()
|
|
243
|
+
for ingestor in ingestors:
|
|
244
|
+
if ingestor.name == name:
|
|
245
|
+
return ingestor
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def get_ingestor_for_source(
|
|
250
|
+
source: IngestorSource,
|
|
251
|
+
ingestors: list[IngestorPlugin] | None = None,
|
|
252
|
+
) -> IngestorPlugin | None:
|
|
253
|
+
"""Auto-detect and return the appropriate ingestor for *source*.
|
|
254
|
+
|
|
255
|
+
Iterates through the available ingestors and returns the first one whose
|
|
256
|
+
:meth:`~IngestorPlugin.can_handle` method returns True.
|
|
257
|
+
|
|
258
|
+
Parameters
|
|
259
|
+
----------
|
|
260
|
+
source:
|
|
261
|
+
The source to find an ingestor for.
|
|
262
|
+
ingestors:
|
|
263
|
+
List of instantiated ingestor objects to search. Defaults to
|
|
264
|
+
:func:`load_all_ingestors` if not provided.
|
|
265
|
+
|
|
266
|
+
Returns
|
|
267
|
+
-------
|
|
268
|
+
IngestorPlugin | None
|
|
269
|
+
The first matching ingestor, or ``None`` if no ingestor can handle
|
|
270
|
+
the source.
|
|
271
|
+
"""
|
|
272
|
+
if ingestors is None:
|
|
273
|
+
ingestors = load_all_ingestors()
|
|
274
|
+
for ingestor in ingestors:
|
|
275
|
+
try:
|
|
276
|
+
if ingestor.can_handle(source):
|
|
277
|
+
return ingestor
|
|
278
|
+
except Exception as exc: # noqa: BLE001
|
|
279
|
+
logger.warning(
|
|
280
|
+
"Ingestor '%s' raised an error in can_handle: %s — skipping.",
|
|
281
|
+
ingestor.name,
|
|
282
|
+
exc,
|
|
283
|
+
)
|
|
284
|
+
return None
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Copyright (C) 2026 Brad Guider
|
|
2
|
+
# This file is part of NAT (Neural Agent Testing Framework).
|
|
3
|
+
# Licensed under the AGPL-3.0. See LICENSE for details.
|
|
4
|
+
# Commercial licensing available — see COMMERCIAL_LICENSE.md.
|
|
5
|
+
|
|
6
|
+
"""Dataclass models for the ingestor plugin architecture.
|
|
7
|
+
|
|
8
|
+
These models represent the unified internal format that all ingestor plugins
|
|
9
|
+
produce when consuming specs, test cases, and other artifacts.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class IngestorSource:
|
|
19
|
+
"""Metadata about the source being ingested.
|
|
20
|
+
|
|
21
|
+
Attributes
|
|
22
|
+
----------
|
|
23
|
+
format:
|
|
24
|
+
Source format identifier (e.g., ``"openapi"``, ``"postman"``,
|
|
25
|
+
``"har"``, ``"graphql"``, ``"gherkin"``, ``"bgstm"``, ``"curl"``).
|
|
26
|
+
path:
|
|
27
|
+
File path if the source is a local file.
|
|
28
|
+
url:
|
|
29
|
+
URL if the source is a remote resource.
|
|
30
|
+
raw_content:
|
|
31
|
+
Raw content string if provided inline.
|
|
32
|
+
metadata:
|
|
33
|
+
Arbitrary metadata from the source.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
format: str
|
|
37
|
+
path: str | None = None
|
|
38
|
+
url: str | None = None
|
|
39
|
+
raw_content: str | None = None
|
|
40
|
+
metadata: dict = field(default_factory=dict)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class IngestedEndpoint:
|
|
45
|
+
"""A single API endpoint/operation extracted from an ingested source.
|
|
46
|
+
|
|
47
|
+
Attributes
|
|
48
|
+
----------
|
|
49
|
+
method:
|
|
50
|
+
HTTP method (GET, POST, PUT, DELETE, etc.).
|
|
51
|
+
path:
|
|
52
|
+
URL path template (e.g., ``"/api/v1/users/{id}"``).
|
|
53
|
+
summary:
|
|
54
|
+
Human-readable summary of the operation.
|
|
55
|
+
description:
|
|
56
|
+
Detailed description of the operation.
|
|
57
|
+
parameters:
|
|
58
|
+
Parameter definitions (query, path, header, cookie).
|
|
59
|
+
request_body:
|
|
60
|
+
Request body schema or example, if applicable.
|
|
61
|
+
responses:
|
|
62
|
+
Expected response schemas keyed by HTTP status code.
|
|
63
|
+
auth_requirements:
|
|
64
|
+
Authentication schemes required for this endpoint.
|
|
65
|
+
tags:
|
|
66
|
+
Categorization tags.
|
|
67
|
+
metadata:
|
|
68
|
+
Format-specific extra data.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
method: str
|
|
72
|
+
path: str
|
|
73
|
+
summary: str = ""
|
|
74
|
+
description: str = ""
|
|
75
|
+
parameters: list[dict] = field(default_factory=list)
|
|
76
|
+
request_body: dict | None = None
|
|
77
|
+
responses: dict = field(default_factory=dict)
|
|
78
|
+
auth_requirements: list[str] = field(default_factory=list)
|
|
79
|
+
tags: list[str] = field(default_factory=list)
|
|
80
|
+
metadata: dict = field(default_factory=dict)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class IngestedTestCase:
|
|
85
|
+
"""A test case extracted from an ingested source (e.g., BDD/Gherkin/BGSTM).
|
|
86
|
+
|
|
87
|
+
Attributes
|
|
88
|
+
----------
|
|
89
|
+
name:
|
|
90
|
+
Test case name.
|
|
91
|
+
description:
|
|
92
|
+
Test case description.
|
|
93
|
+
steps:
|
|
94
|
+
Ordered list of test steps.
|
|
95
|
+
expected_result:
|
|
96
|
+
Expected outcome of the test.
|
|
97
|
+
priority:
|
|
98
|
+
Priority level (``"critical"``, ``"high"``, ``"medium"``, ``"low"``).
|
|
99
|
+
tags:
|
|
100
|
+
Categorization tags.
|
|
101
|
+
source_ref:
|
|
102
|
+
Reference back to the source (file:line, scenario name, etc.).
|
|
103
|
+
metadata:
|
|
104
|
+
Format-specific extra data.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
name: str
|
|
108
|
+
description: str = ""
|
|
109
|
+
steps: list[dict] = field(default_factory=list)
|
|
110
|
+
expected_result: str = ""
|
|
111
|
+
priority: str = "medium"
|
|
112
|
+
tags: list[str] = field(default_factory=list)
|
|
113
|
+
source_ref: str = ""
|
|
114
|
+
metadata: dict = field(default_factory=dict)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass
|
|
118
|
+
class IngestResult:
|
|
119
|
+
"""The complete output of an ingestion operation.
|
|
120
|
+
|
|
121
|
+
Attributes
|
|
122
|
+
----------
|
|
123
|
+
success:
|
|
124
|
+
Whether ingestion succeeded.
|
|
125
|
+
source:
|
|
126
|
+
The source that was ingested.
|
|
127
|
+
endpoints:
|
|
128
|
+
Extracted API endpoints.
|
|
129
|
+
test_cases:
|
|
130
|
+
Extracted test cases.
|
|
131
|
+
errors:
|
|
132
|
+
Any errors encountered during ingestion.
|
|
133
|
+
warnings:
|
|
134
|
+
Any warnings (non-fatal issues).
|
|
135
|
+
stats:
|
|
136
|
+
Summary statistics (e.g., ``endpoint_count``, ``test_case_count``,
|
|
137
|
+
``duration_ms``).
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
success: bool
|
|
141
|
+
source: IngestorSource
|
|
142
|
+
endpoints: list[IngestedEndpoint] = field(default_factory=list)
|
|
143
|
+
test_cases: list[IngestedTestCase] = field(default_factory=list)
|
|
144
|
+
errors: list[str] = field(default_factory=list)
|
|
145
|
+
warnings: list[str] = field(default_factory=list)
|
|
146
|
+
stats: dict = field(default_factory=dict)
|