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.
Files changed (299) hide show
  1. mannf/__init__.py +33 -0
  2. mannf/__main__.py +10 -0
  3. mannf/_version.py +8 -0
  4. mannf/agents/__init__.py +7 -0
  5. mannf/agents/analyzer_agent.py +9 -0
  6. mannf/agents/base.py +9 -0
  7. mannf/agents/bdi_agent.py +9 -0
  8. mannf/agents/belief_state.py +9 -0
  9. mannf/agents/coordinator_agent.py +9 -0
  10. mannf/agents/executor_agent.py +9 -0
  11. mannf/agents/monitor_agent.py +9 -0
  12. mannf/agents/oracle_agent.py +9 -0
  13. mannf/agents/planner_agent.py +9 -0
  14. mannf/agents/test_agent.py +9 -0
  15. mannf/anomaly/__init__.py +7 -0
  16. mannf/anomaly/enhanced_detector.py +9 -0
  17. mannf/cli.py +9 -0
  18. mannf/core/__init__.py +26 -0
  19. mannf/core/agents/__init__.py +52 -0
  20. mannf/core/agents/accessibility_scanner_agent.py +245 -0
  21. mannf/core/agents/analyzer_agent.py +224 -0
  22. mannf/core/agents/autonomous_loop_agent.py +1086 -0
  23. mannf/core/agents/autonomous_loop_models.py +62 -0
  24. mannf/core/agents/autonomous_run_differ.py +427 -0
  25. mannf/core/agents/base.py +128 -0
  26. mannf/core/agents/bdi_agent.py +330 -0
  27. mannf/core/agents/belief_state.py +202 -0
  28. mannf/core/agents/browser_coordinator_agent.py +224 -0
  29. mannf/core/agents/browser_executor_agent.py +410 -0
  30. mannf/core/agents/coordinator_agent.py +262 -0
  31. mannf/core/agents/executor_agent.py +222 -0
  32. mannf/core/agents/monitor_agent.py +188 -0
  33. mannf/core/agents/oracle_agent.py +150 -0
  34. mannf/core/agents/performance_testing_agent.py +279 -0
  35. mannf/core/agents/planner_agent.py +128 -0
  36. mannf/core/agents/test_agent.py +249 -0
  37. mannf/core/agents/visual_regression_agent.py +311 -0
  38. mannf/core/agents/web_crawler_agent.py +510 -0
  39. mannf/core/agents/worker_pool.py +366 -0
  40. mannf/core/anomaly/__init__.py +14 -0
  41. mannf/core/anomaly/enhanced_detector.py +541 -0
  42. mannf/core/browser/__init__.py +63 -0
  43. mannf/core/browser/accessibility_scanner.py +424 -0
  44. mannf/core/browser/discovery_model.py +178 -0
  45. mannf/core/browser/dom_snapshot.py +349 -0
  46. mannf/core/browser/ingestor_bridge.py +371 -0
  47. mannf/core/browser/performance_metrics.py +217 -0
  48. mannf/core/browser/reflection_analyzer.py +442 -0
  49. mannf/core/browser/scenario_generator.py +1100 -0
  50. mannf/core/browser/security_scenario_generator.py +695 -0
  51. mannf/core/browser/visual_comparer.py +159 -0
  52. mannf/core/diagnostics/__init__.py +28 -0
  53. mannf/core/diagnostics/failure_clusterer.py +211 -0
  54. mannf/core/diagnostics/flake_detector.py +233 -0
  55. mannf/core/diagnostics/root_cause_analyzer.py +273 -0
  56. mannf/core/distributed/__init__.py +16 -0
  57. mannf/core/distributed/endpoint.py +139 -0
  58. mannf/core/distributed/system_under_test.py +207 -0
  59. mannf/core/functional_orchestrator.py +428 -0
  60. mannf/core/messaging/__init__.py +11 -0
  61. mannf/core/messaging/bus.py +113 -0
  62. mannf/core/messaging/messages.py +89 -0
  63. mannf/core/nat_orchestrator.py +342 -0
  64. mannf/core/neural/__init__.py +183 -0
  65. mannf/core/orchestrator.py +272 -0
  66. mannf/core/prioritization/__init__.py +17 -0
  67. mannf/core/prioritization/adaptive_controller.py +509 -0
  68. mannf/core/prioritization/belief_prioritizer.py +231 -0
  69. mannf/core/prioritization/risk_scorer.py +430 -0
  70. mannf/core/reporting/__init__.py +12 -0
  71. mannf/core/reporting/unified_report.py +664 -0
  72. mannf/core/testing/__init__.py +17 -0
  73. mannf/core/testing/adaptive_controller.py +149 -0
  74. mannf/core/testing/models.py +179 -0
  75. mannf/core/validation/__init__.py +10 -0
  76. mannf/core/validation/self_validation_runner.py +180 -0
  77. mannf/dashboard/__init__.py +7 -0
  78. mannf/dashboard/app.py +9 -0
  79. mannf/dashboard/models.py +9 -0
  80. mannf/dashboard/static/index.html +2538 -0
  81. mannf/dashboard/telemetry.py +9 -0
  82. mannf/distributed/__init__.py +7 -0
  83. mannf/distributed/endpoint.py +9 -0
  84. mannf/distributed/system_under_test.py +9 -0
  85. mannf/healing/__init__.py +7 -0
  86. mannf/healing/graphql_schema_diff.py +9 -0
  87. mannf/healing/healer.py +9 -0
  88. mannf/healing/models.py +9 -0
  89. mannf/healing/schema_diff.py +9 -0
  90. mannf/integrations/__init__.py +7 -0
  91. mannf/integrations/auth.py +9 -0
  92. mannf/integrations/graphql_parser.py +9 -0
  93. mannf/integrations/graphql_sut.py +9 -0
  94. mannf/integrations/http_sut.py +9 -0
  95. mannf/integrations/openapi_parser.py +9 -0
  96. mannf/integrations/postman_parser.py +9 -0
  97. mannf/llm/__init__.py +7 -0
  98. mannf/llm/anthropic_provider.py +9 -0
  99. mannf/llm/base.py +9 -0
  100. mannf/llm/config.py +9 -0
  101. mannf/llm/factory.py +9 -0
  102. mannf/llm/openai_provider.py +9 -0
  103. mannf/llm/prompts.py +9 -0
  104. mannf/messaging/__init__.py +7 -0
  105. mannf/messaging/bus.py +9 -0
  106. mannf/messaging/messages.py +9 -0
  107. mannf/nat_orchestrator.py +9 -0
  108. mannf/neural/__init__.py +7 -0
  109. mannf/orchestrator.py +9 -0
  110. mannf/prioritization/__init__.py +7 -0
  111. mannf/prioritization/adaptive_controller.py +9 -0
  112. mannf/prioritization/belief_prioritizer.py +9 -0
  113. mannf/prioritization/risk_scorer.py +9 -0
  114. mannf/product/__init__.py +29 -0
  115. mannf/product/admin/__init__.py +3 -0
  116. mannf/product/admin/routes.py +514 -0
  117. mannf/product/auth/__init__.py +5 -0
  118. mannf/product/auth/saml.py +212 -0
  119. mannf/product/billing/__init__.py +5 -0
  120. mannf/product/billing/audit.py +160 -0
  121. mannf/product/billing/feature_gates.py +180 -0
  122. mannf/product/billing/metering.py +179 -0
  123. mannf/product/billing/notifications.py +181 -0
  124. mannf/product/billing/plans.py +133 -0
  125. mannf/product/billing/rate_limits.py +35 -0
  126. mannf/product/billing/stripe_billing.py +906 -0
  127. mannf/product/billing/tenant_auth.py +233 -0
  128. mannf/product/billing/tenant_manager.py +873 -0
  129. mannf/product/cli.py +3900 -0
  130. mannf/product/cli_admin.py +408 -0
  131. mannf/product/dashboard/__init__.py +61 -0
  132. mannf/product/dashboard/app.py +3567 -0
  133. mannf/product/dashboard/models.py +460 -0
  134. mannf/product/dashboard/static/index.html +6347 -0
  135. mannf/product/dashboard/static/manifest.json +25 -0
  136. mannf/product/dashboard/static/pwa-icon-192.png +0 -0
  137. mannf/product/dashboard/static/pwa-icon-512.png +0 -0
  138. mannf/product/dashboard/static/sw.js +64 -0
  139. mannf/product/dashboard/telemetry.py +547 -0
  140. mannf/product/database.py +145 -0
  141. mannf/product/demo.py +844 -0
  142. mannf/product/doctor.py +509 -0
  143. mannf/product/exporters/__init__.py +65 -0
  144. mannf/product/exporters/azuredevops_exporter.py +257 -0
  145. mannf/product/exporters/base.py +307 -0
  146. mannf/product/exporters/bugzilla_exporter.py +200 -0
  147. mannf/product/exporters/dedup.py +275 -0
  148. mannf/product/exporters/finding_adapter.py +216 -0
  149. mannf/product/exporters/github_exporter.py +197 -0
  150. mannf/product/exporters/gitlab_exporter.py +215 -0
  151. mannf/product/exporters/jira_exporter.py +180 -0
  152. mannf/product/exporters/linear_exporter.py +195 -0
  153. mannf/product/exporters/loader.py +233 -0
  154. mannf/product/exporters/pagerduty_exporter.py +363 -0
  155. mannf/product/exporters/sentry_exporter.py +322 -0
  156. mannf/product/exporters/servicenow_exporter.py +240 -0
  157. mannf/product/exporters/shortcut_exporter.py +231 -0
  158. mannf/product/exporters/webhook_exporter.py +383 -0
  159. mannf/product/formatters/__init__.py +18 -0
  160. mannf/product/formatters/allure_formatter.py +161 -0
  161. mannf/product/formatters/ctrf_formatter.py +149 -0
  162. mannf/product/healing/__init__.py +30 -0
  163. mannf/product/healing/graphql_schema_diff.py +152 -0
  164. mannf/product/healing/healer.py +141 -0
  165. mannf/product/healing/models.py +175 -0
  166. mannf/product/healing/schema_diff.py +251 -0
  167. mannf/product/ingestors/__init__.py +77 -0
  168. mannf/product/ingestors/base.py +256 -0
  169. mannf/product/ingestors/bgstm_ingestor.py +764 -0
  170. mannf/product/ingestors/curl_ingestor.py +1019 -0
  171. mannf/product/ingestors/cypress_ingestor.py +487 -0
  172. mannf/product/ingestors/gherkin_ingestor.py +967 -0
  173. mannf/product/ingestors/graphql_ingestor.py +845 -0
  174. mannf/product/ingestors/grpc_ingestor.py +591 -0
  175. mannf/product/ingestors/har_ingestor.py +976 -0
  176. mannf/product/ingestors/loader.py +284 -0
  177. mannf/product/ingestors/models.py +146 -0
  178. mannf/product/ingestors/openapi_ingestor.py +606 -0
  179. mannf/product/ingestors/playwright_ingestor.py +449 -0
  180. mannf/product/ingestors/postman_ingestor.py +631 -0
  181. mannf/product/ingestors/traffic_ingestor.py +679 -0
  182. mannf/product/ingestors/websocket_ingestor.py +526 -0
  183. mannf/product/integrations/__init__.py +21 -0
  184. mannf/product/integrations/auth.py +190 -0
  185. mannf/product/integrations/graphql_parser.py +436 -0
  186. mannf/product/integrations/graphql_sut.py +247 -0
  187. mannf/product/integrations/grpc_sut.py +469 -0
  188. mannf/product/integrations/http_sut.py +237 -0
  189. mannf/product/integrations/kafka_adapter.py +342 -0
  190. mannf/product/integrations/openapi_parser.py +513 -0
  191. mannf/product/integrations/postman_parser.py +467 -0
  192. mannf/product/integrations/webhook_receiver.py +344 -0
  193. mannf/product/integrations/websocket_sut.py +434 -0
  194. mannf/product/llm/__init__.py +25 -0
  195. mannf/product/llm/anthropic_provider.py +94 -0
  196. mannf/product/llm/base.py +267 -0
  197. mannf/product/llm/config.py +48 -0
  198. mannf/product/llm/factory.py +42 -0
  199. mannf/product/llm/openai_provider.py +93 -0
  200. mannf/product/llm/prompts.py +403 -0
  201. mannf/product/llm/root_cause_service.py +311 -0
  202. mannf/product/llm/test_plan_models.py +78 -0
  203. mannf/product/metrics.py +149 -0
  204. mannf/product/middleware/__init__.py +3 -0
  205. mannf/product/middleware/audit_middleware.py +112 -0
  206. mannf/product/middleware/tenant_isolation.py +114 -0
  207. mannf/product/models.py +347 -0
  208. mannf/product/notifications/__init__.py +24 -0
  209. mannf/product/notifications/dispatcher.py +411 -0
  210. mannf/product/onboarding.py +190 -0
  211. mannf/product/orchestration/__init__.py +39 -0
  212. mannf/product/orchestration/ingest_scan_orchestrator.py +339 -0
  213. mannf/product/orchestration/pipeline.py +401 -0
  214. mannf/product/orchestrator.py +987 -0
  215. mannf/product/orchestrator_models.py +269 -0
  216. mannf/product/regression/__init__.py +36 -0
  217. mannf/product/regression/differ.py +172 -0
  218. mannf/product/regression/masking.py +100 -0
  219. mannf/product/regression/models.py +232 -0
  220. mannf/product/regression/recorder.py +124 -0
  221. mannf/product/regression/replayer.py +168 -0
  222. mannf/product/reports/__init__.py +10 -0
  223. mannf/product/reports/pdf.py +132 -0
  224. mannf/product/scheduling/__init__.py +57 -0
  225. mannf/product/scheduling/cron_utils.py +251 -0
  226. mannf/product/scheduling/engine.py +473 -0
  227. mannf/product/scheduling/models.py +86 -0
  228. mannf/product/scheduling/queue.py +894 -0
  229. mannf/product/scheduling/store.py +235 -0
  230. mannf/product/security/__init__.py +21 -0
  231. mannf/product/security/belief_guided.py +143 -0
  232. mannf/product/security/checks/__init__.py +55 -0
  233. mannf/product/security/checks/base.py +69 -0
  234. mannf/product/security/checks/bfla.py +77 -0
  235. mannf/product/security/checks/bola.py +77 -0
  236. mannf/product/security/checks/bopla.py +80 -0
  237. mannf/product/security/checks/broken_auth.py +86 -0
  238. mannf/product/security/checks/graphql_security.py +299 -0
  239. mannf/product/security/checks/inventory.py +70 -0
  240. mannf/product/security/checks/misconfig.py +158 -0
  241. mannf/product/security/checks/resource_consumption.py +70 -0
  242. mannf/product/security/checks/sensitive_flows.py +80 -0
  243. mannf/product/security/checks/ssrf.py +101 -0
  244. mannf/product/security/checks/unsafe_consumption.py +120 -0
  245. mannf/product/security/models.py +92 -0
  246. mannf/product/security/plugin_loader.py +182 -0
  247. mannf/product/security/reporter.py +92 -0
  248. mannf/product/security/scanner.py +183 -0
  249. mannf/product/server.py +6220 -0
  250. mannf/product/setup_wizard.py +873 -0
  251. mannf/product/status.py +404 -0
  252. mannf/product/storage/__init__.py +10 -0
  253. mannf/product/storage/artifact_store.py +343 -0
  254. mannf/product/telemetry.py +300 -0
  255. mannf/product/uninstall.py +169 -0
  256. mannf/product/upgrade.py +139 -0
  257. mannf/product/weights/__init__.py +13 -0
  258. mannf/product/weights/blob_store.py +299 -0
  259. mannf/product/weights/factory.py +42 -0
  260. mannf/product/weights/registry.py +159 -0
  261. mannf/product/weights/store.py +210 -0
  262. mannf/regression/__init__.py +7 -0
  263. mannf/regression/differ.py +9 -0
  264. mannf/regression/masking.py +9 -0
  265. mannf/regression/models.py +9 -0
  266. mannf/regression/recorder.py +9 -0
  267. mannf/regression/replayer.py +9 -0
  268. mannf/security/__init__.py +7 -0
  269. mannf/security/belief_guided.py +9 -0
  270. mannf/security/checks/__init__.py +7 -0
  271. mannf/security/checks/base.py +9 -0
  272. mannf/security/checks/bfla.py +9 -0
  273. mannf/security/checks/bola.py +9 -0
  274. mannf/security/checks/bopla.py +9 -0
  275. mannf/security/checks/broken_auth.py +9 -0
  276. mannf/security/checks/graphql_security.py +9 -0
  277. mannf/security/checks/inventory.py +9 -0
  278. mannf/security/checks/misconfig.py +9 -0
  279. mannf/security/checks/resource_consumption.py +9 -0
  280. mannf/security/checks/sensitive_flows.py +9 -0
  281. mannf/security/checks/ssrf.py +9 -0
  282. mannf/security/checks/unsafe_consumption.py +9 -0
  283. mannf/security/models.py +9 -0
  284. mannf/security/reporter.py +9 -0
  285. mannf/security/scanner.py +9 -0
  286. mannf/server.py +9 -0
  287. mannf/testing/__init__.py +7 -0
  288. mannf/testing/adaptive_controller.py +9 -0
  289. mannf/testing/models.py +9 -0
  290. mannf/weights/__init__.py +7 -0
  291. mannf/weights/registry.py +9 -0
  292. mannf/weights/store.py +9 -0
  293. nat_engine-1.dist-info/METADATA +555 -0
  294. nat_engine-1.dist-info/RECORD +299 -0
  295. nat_engine-1.dist-info/WHEEL +5 -0
  296. nat_engine-1.dist-info/entry_points.txt +4 -0
  297. nat_engine-1.dist-info/licenses/LICENSE +651 -0
  298. nat_engine-1.dist-info/licenses/NOTICE +178 -0
  299. nat_engine-1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,233 @@
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
+
5
+ """Tenant authentication and quota checking helpers.
6
+
7
+ These functions bridge the auth middleware with the Tenant/ApiKey/UsageRecord
8
+ database models from PR 1. They are designed to gracefully degrade — if no
9
+ DATABASE_URL is configured or no tenants exist in the DB, the system falls
10
+ back to the legacy NAT_API_KEY env var behavior.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import hashlib
16
+ import logging
17
+ import uuid
18
+ from datetime import datetime, timezone
19
+ from typing import Optional, Tuple
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ async def resolve_tenant_from_key(api_key: str) -> Optional[Tuple[dict, dict]]:
25
+ """Look up an API key by its SHA-256 hash in the database.
26
+
27
+ Returns (tenant_dict, api_key_dict) if found and both are active,
28
+ or None if not found / inactive / no DB.
29
+
30
+ The tenant_dict includes: id, name, email, plan_tier,
31
+ monthly_scan_quota, monthly_security_scan_quota, is_active.
32
+
33
+ The api_key_dict includes: id, tenant_id, key_prefix, label, is_active.
34
+
35
+ Also updates the api_key's last_used_at timestamp (fire-and-forget,
36
+ don't block on it).
37
+ """
38
+ try:
39
+ from mannf.product.database import _async_session_factory, _build_engine
40
+
41
+ if _build_engine() is None or _async_session_factory is None:
42
+ return None
43
+
44
+ from sqlalchemy import select
45
+ from mannf.product.models import ApiKey, Tenant
46
+
47
+ key_hash = hashlib.sha256(api_key.encode()).hexdigest()
48
+
49
+ async with _async_session_factory() as session:
50
+ stmt = (
51
+ select(ApiKey, Tenant)
52
+ .join(Tenant, ApiKey.tenant_id == Tenant.id)
53
+ .where(ApiKey.key_hash == key_hash)
54
+ .where(ApiKey.is_active.is_(True))
55
+ .where(Tenant.is_active.is_(True))
56
+ )
57
+ result = await session.execute(stmt)
58
+ row = result.first()
59
+ if row is None:
60
+ return None
61
+
62
+ api_key_obj, tenant_obj = row
63
+
64
+ tenant_dict = {
65
+ "id": str(tenant_obj.id),
66
+ "name": tenant_obj.name,
67
+ "email": tenant_obj.email,
68
+ "plan_tier": tenant_obj.plan_tier,
69
+ "monthly_scan_quota": tenant_obj.monthly_scan_quota,
70
+ "monthly_security_scan_quota": tenant_obj.monthly_security_scan_quota,
71
+ "max_concurrent_scans": getattr(tenant_obj, "max_concurrent_scans", 1),
72
+ "is_active": tenant_obj.is_active,
73
+ }
74
+ api_key_dict = {
75
+ "id": str(api_key_obj.id),
76
+ "tenant_id": str(api_key_obj.tenant_id),
77
+ "key_prefix": api_key_obj.key_prefix,
78
+ "label": api_key_obj.label,
79
+ "is_active": api_key_obj.is_active,
80
+ }
81
+
82
+ # Fire-and-forget: update last_used_at
83
+ import asyncio
84
+
85
+ async def _update_last_used(ak_id: uuid.UUID) -> None:
86
+ try:
87
+ async with _async_session_factory() as s:
88
+ obj = await s.get(ApiKey, ak_id)
89
+ if obj is not None:
90
+ obj.last_used_at = datetime.now(timezone.utc)
91
+ await s.commit()
92
+ except Exception as exc: # noqa: BLE001
93
+ logger.debug("last_used_at update failed: %s", exc)
94
+
95
+ asyncio.create_task(_update_last_used(api_key_obj.id))
96
+
97
+ return tenant_dict, api_key_dict
98
+
99
+ except ImportError:
100
+ return None
101
+ except Exception as exc: # noqa: BLE001
102
+ logger.warning("resolve_tenant_from_key failed: %s", exc)
103
+ return None
104
+
105
+
106
+ async def get_monthly_usage(tenant_id: uuid.UUID, scan_type: str) -> int:
107
+ """Count the number of scans of the given type this tenant has used
108
+ in the current calendar month.
109
+
110
+ Uses the composite index ix_usage_records_tenant_period for fast lookup.
111
+ Returns 0 if no DB is configured.
112
+ """
113
+ try:
114
+ from mannf.product.database import _async_session_factory, _build_engine
115
+
116
+ if _build_engine() is None or _async_session_factory is None:
117
+ return 0
118
+
119
+ from sqlalchemy import func, select
120
+ from mannf.product.models import UsageRecord
121
+
122
+ now = datetime.now(timezone.utc)
123
+ async with _async_session_factory() as session:
124
+ stmt = (
125
+ select(func.count())
126
+ .select_from(UsageRecord)
127
+ .where(UsageRecord.tenant_id == tenant_id)
128
+ .where(UsageRecord.scan_type == scan_type)
129
+ .where(UsageRecord.period_year == now.year)
130
+ .where(UsageRecord.period_month == now.month)
131
+ )
132
+ result = await session.execute(stmt)
133
+ return result.scalar() or 0
134
+
135
+ except ImportError:
136
+ return 0
137
+ except Exception as exc: # noqa: BLE001
138
+ logger.warning("get_monthly_usage failed: %s", exc)
139
+ return 0
140
+
141
+
142
+ async def record_usage(
143
+ tenant_id: uuid.UUID,
144
+ scan_type: str,
145
+ scan_id: Optional[uuid.UUID] = None,
146
+ ) -> None:
147
+ """Insert a UsageRecord row for this tenant/scan.
148
+
149
+ Sets period_year and period_month from the current UTC date.
150
+ No-op if no DB is configured.
151
+ """
152
+ try:
153
+ from mannf.product.database import _async_session_factory, _build_engine
154
+
155
+ if _build_engine() is None or _async_session_factory is None:
156
+ return
157
+
158
+ from mannf.product.models import UsageRecord
159
+
160
+ now = datetime.now(timezone.utc)
161
+ async with _async_session_factory() as session:
162
+ record = UsageRecord(
163
+ id=uuid.uuid4(),
164
+ tenant_id=tenant_id,
165
+ scan_type=scan_type,
166
+ scan_id=scan_id,
167
+ recorded_at=now,
168
+ period_year=now.year,
169
+ period_month=now.month,
170
+ )
171
+ session.add(record)
172
+ await session.commit()
173
+
174
+ except ImportError:
175
+ return
176
+ except Exception as exc: # noqa: BLE001
177
+ logger.warning("record_usage failed: %s", exc)
178
+
179
+
180
+ async def get_daily_usage(tenant_id: uuid.UUID, scan_type: str) -> list:
181
+ """Return per-day usage counts for the current calendar month.
182
+
183
+ Each item is a dict with ``date`` (YYYY-MM-DD string) and ``count``
184
+ (number of scans of *scan_type* recorded on that date).
185
+ Returns an empty list when no DB is configured.
186
+ """
187
+ try:
188
+ from mannf.product.database import _async_session_factory, _build_engine
189
+
190
+ if _build_engine() is None or _async_session_factory is None:
191
+ return []
192
+
193
+ from sqlalchemy import cast, func, select
194
+ from sqlalchemy import Date
195
+ from mannf.product.models import UsageRecord
196
+
197
+ now = datetime.now(timezone.utc)
198
+ async with _async_session_factory() as session:
199
+ stmt = (
200
+ select(
201
+ cast(UsageRecord.recorded_at, Date).label("day"),
202
+ func.count().label("count"),
203
+ )
204
+ .where(UsageRecord.tenant_id == tenant_id)
205
+ .where(UsageRecord.scan_type == scan_type)
206
+ .where(UsageRecord.period_year == now.year)
207
+ .where(UsageRecord.period_month == now.month)
208
+ .group_by(cast(UsageRecord.recorded_at, Date))
209
+ .order_by(cast(UsageRecord.recorded_at, Date))
210
+ )
211
+ result = await session.execute(stmt)
212
+ rows = result.all()
213
+ return [{"date": str(row.day), "count": row.count} for row in rows]
214
+
215
+ except ImportError:
216
+ return []
217
+ except Exception as exc: # noqa: BLE001
218
+ logger.warning("get_daily_usage failed: %s", exc)
219
+ return []
220
+
221
+
222
+ async def check_quota(
223
+ tenant_id: uuid.UUID,
224
+ scan_type: str,
225
+ tenant_quota: int,
226
+ ) -> Tuple[bool, int, int]:
227
+ """Check if the tenant is within their monthly quota.
228
+
229
+ Returns (allowed: bool, used: int, limit: int).
230
+ """
231
+ used = await get_monthly_usage(tenant_id, scan_type)
232
+ allowed = used < tenant_quota
233
+ return allowed, used, tenant_quota