ci: replace artifact upload with Gitea release API and add structured logging with rate limiting

- Replaced actions/upload-artifact with Gitea API release creation in build-miner-binary.yml
- Added separate steps for uploading binary, package, and checksums to Gitea release
- Added StructuredFormatter class for JSON log output in aitbc_logging.py
- Added structured logging support with log_context() context manager and LogContext class
- Added structured parameter to setup_logger() and configure_logging()
This commit is contained in:
aitbc
2026-05-12 21:33:20 +02:00
parent 40cee6d791
commit a266b3b70e
25 changed files with 1464 additions and 129 deletions

View File

@@ -77,11 +77,54 @@ jobs:
echo "version=$VERSION" >> $GITHUB_OUTPUT echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "version=$VERSION" echo "version=$VERSION"
- name: Upload artifacts - name: Create Gitea release
uses: actions/upload-artifact@v3 run: |
with: cd "${{ env.WORKSPACE }}/repo"
name: miner-binary VERSION=${GITHUB_REF#refs/tags/v}
path: |
${{ env.WORKSPACE }}/repo/scripts/gpu/aitbc-miner-debian # Create release using Gitea API
${{ env.WORKSPACE }}/repo/scripts/gpu/aitbc-miner-debian-package.tar.gz curl -X POST \
${{ env.WORKSPACE }}/repo/scripts/gpu/SHA256SUMS -H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
-H "Content-Type: application/json" \
http://gitea.bubuit.net:3000/api/v1/repos/oib/aitbc/releases \
-d "{
\"tag_name\": \"v${VERSION}\",
\"target_commitish\": \"main\",
\"name\": \"AITBC Miner v${VERSION}\",
\"body\": \"AITBC Miner for Debian Stable (trixie)\\n\\n## Changes\\n- See commit history for details\",
\"draft\": false,
\"prerelease\": false
}"
- name: Upload binary to Gitea release
run: |
cd "${{ env.WORKSPACE }}/repo"
VERSION=${GITHUB_REF#refs/tags/v}
# Upload binary
curl -X POST \
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
-F "attachment=@scripts/gpu/aitbc-miner-debian" \
http://gitea.bubuit.net:3000/api/v1/repos/oib/aitbc/releases/v${VERSION}/assets
- name: Upload package to Gitea release
run: |
cd "${{ env.WORKSPACE }}/repo"
VERSION=${GITHUB_REF#refs/tags/v}
# Upload package
curl -X POST \
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
-F "attachment=@scripts/gpu/aitbc-miner-debian-package.tar.gz" \
http://gitea.bubuit.net:3000/api/v1/repos/oib/aitbc/releases/v${VERSION}/assets
- name: Upload checksums to Gitea release
run: |
cd "${{ env.WORKSPACE }}/repo"
VERSION=${GITHUB_REF#refs/tags/v}
# Upload checksums
curl -X POST \
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
-F "attachment=@scripts/gpu/SHA256SUMS" \
http://gitea.bubuit.net:3000/api/v1/repos/oib/aitbc/releases/v${VERSION}/assets

View File

@@ -5,23 +5,56 @@ Centralized logging utilities for the AITBC project
import logging import logging
import sys import sys
from typing import Optional import json
from typing import Optional, Dict, Any
from datetime import datetime
from contextlib import contextmanager
class StructuredFormatter(logging.Formatter):
"""Structured JSON formatter for log aggregation"""
def format(self, record: logging.LogRecord) -> str:
"""Format log record as structured JSON"""
log_entry = {
"timestamp": datetime.utcnow().isoformat() + "Z",
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
"module": record.module,
"function": record.funcName,
"line": record.lineno,
}
# Add extra fields if present
if hasattr(record, 'extra'):
log_entry.update(record.extra)
# Add exception info if present
if record.exc_info:
log_entry["exception"] = self.formatException(record.exc_info)
return json.dumps(log_entry)
def setup_logger( def setup_logger(
name: str, name: str,
level: str = "INFO", level: str = "INFO",
format_string: Optional[str] = None format_string: Optional[str] = None,
structured: bool = False
) -> logging.Logger: ) -> logging.Logger:
"""Setup a logger with consistent formatting""" """Setup a logger with consistent formatting"""
if format_string is None:
format_string = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
logger = logging.getLogger(name) logger = logging.getLogger(name)
logger.setLevel(getattr(logging, level.upper())) logger.setLevel(getattr(logging, level.upper()))
if not logger.handlers: if not logger.handlers:
handler = logging.StreamHandler(sys.stdout) handler = logging.StreamHandler(sys.stdout)
if structured:
formatter = StructuredFormatter()
else:
if format_string is None:
format_string = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
formatter = logging.Formatter(format_string) formatter = logging.Formatter(format_string)
handler.setFormatter(formatter) handler.setFormatter(formatter)
logger.addHandler(handler) logger.addHandler(handler)
@@ -31,6 +64,48 @@ def get_logger(name: str) -> logging.Logger:
"""Get a logger instance""" """Get a logger instance"""
return logging.getLogger(name) return logging.getLogger(name)
def configure_logging(level: str = "INFO") -> None: def configure_logging(level: str = "INFO", structured: bool = False) -> None:
"""Configure root logging level""" """Configure root logging level"""
logging.basicConfig(level=getattr(logging, level.upper())) root_logger = logging.getLogger()
root_logger.setLevel(getattr(logging, level.upper()))
if structured:
# Remove existing handlers
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
# Add structured handler
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(StructuredFormatter())
root_logger.addHandler(handler)
@contextmanager
def log_context(**kwargs):
"""Context manager for adding contextual information to logs"""
logger = logging.getLogger()
extra = {'extra': kwargs}
class ContextFilter(logging.Filter):
def filter(self, record):
for key, value in kwargs.items():
setattr(record, key, value)
return True
context_filter = ContextFilter()
logger.addFilter(context_filter)
try:
yield
finally:
logger.removeFilter(context_filter)
class LogContext:
"""Class for adding contextual information to logs across multiple calls"""
def __init__(self, **kwargs):
self.context = kwargs
def __enter__(self):
return log_context(**self.context).__enter__()
def __exit__(self, exc_type, exc_val, exc_tb):
pass

414
aitbc/alerting.py Normal file
View File

@@ -0,0 +1,414 @@
"""
AITBC Alerting Module
Alerting and notification system for AITBC applications
"""
import asyncio
from typing import Callable, Dict, Any, List, Optional
from datetime import datetime, timedelta
from dataclasses import dataclass, field
from enum import Enum
import json
from .aitbc_logging import get_logger
logger = get_logger(__name__)
class AlertSeverity(Enum):
"""Alert severity levels"""
INFO = "info"
WARNING = "warning"
ERROR = "error"
CRITICAL = "critical"
class AlertStatus(Enum):
"""Alert status"""
ACTIVE = "active"
ACKNOWLEDGED = "acknowledged"
RESOLVED = "resolved"
@dataclass
class Alert:
"""Alert data structure"""
id: str
severity: AlertSeverity
title: str
message: str
source: str
timestamp: datetime = field(default_factory=datetime.utcnow)
status: AlertStatus = AlertStatus.ACTIVE
metadata: Dict[str, Any] = field(default_factory=dict)
acknowledged_by: Optional[str] = None
acknowledged_at: Optional[datetime] = None
resolved_at: Optional[datetime] = None
def to_dict(self) -> Dict[str, Any]:
"""Convert alert to dictionary"""
return {
"id": self.id,
"severity": self.severity.value,
"title": self.title,
"message": self.message,
"source": self.source,
"timestamp": self.timestamp.isoformat(),
"status": self.status.value,
"metadata": self.metadata,
"acknowledged_by": self.acknowledged_by,
"acknowledged_at": self.acknowledged_at.isoformat() if self.acknowledged_at else None,
"resolved_at": self.resolved_at.isoformat() if self.resolved_at else None,
}
class AlertChannel:
"""Base class for alert channels"""
async def send(self, alert: Alert) -> bool:
"""
Send alert through this channel
Args:
alert: Alert to send
Returns:
True if sent successfully, False otherwise
"""
raise NotImplementedError
class LogAlertChannel(AlertChannel):
"""Log-based alert channel"""
async def send(self, alert: Alert) -> bool:
"""Send alert to logs"""
try:
log_level = {
AlertSeverity.INFO: logger.info,
AlertSeverity.WARNING: logger.warning,
AlertSeverity.ERROR: logger.error,
AlertSeverity.CRITICAL: logger.critical,
}.get(alert.severity, logger.info)
log_level(
f"Alert [{alert.severity.value.upper()}]: {alert.title}",
extra={
"alert_id": alert.id,
"severity": alert.severity.value,
"source": alert.source,
"metadata": alert.metadata,
}
)
return True
except Exception as e:
logger.error(f"Failed to send log alert: {e}")
return False
class WebhookAlertChannel(AlertChannel):
"""Webhook-based alert channel"""
def __init__(self, url: str, headers: Optional[Dict[str, str]] = None):
"""
Initialize webhook channel
Args:
url: Webhook URL
headers: HTTP headers
"""
self.url = url
self.headers = headers or {}
async def send(self, alert: Alert) -> bool:
"""Send alert via webhook"""
try:
import httpx
async with httpx.AsyncClient() as client:
response = await client.post(
self.url,
json=alert.to_dict(),
headers=self.headers,
timeout=10.0
)
response.raise_for_status()
return True
except Exception as e:
logger.error(f"Failed to send webhook alert: {e}")
return False
class AlertRule:
"""Alert rule definition"""
def __init__(
self,
name: str,
condition: Callable[[], bool],
severity: AlertSeverity,
title_template: str,
message_template: str,
source: str,
check_interval: int = 60,
cooldown: int = 300,
metadata: Optional[Dict[str, Any]] = None
):
"""
Initialize alert rule
Args:
name: Rule name
condition: Function that returns True if alert should fire
severity: Alert severity
title_template: Template for alert title
message_template: Template for alert message
source: Alert source
check_interval: Check interval in seconds
cooldown: Cooldown period in seconds
metadata: Additional metadata
"""
self.name = name
self.condition = condition
self.severity = severity
self.title_template = title_template
self.message_template = message_template
self.source = source
self.check_interval = check_interval
self.cooldown = cooldown
self.metadata = metadata or {}
self.last_fired: Optional[datetime] = None
self.enabled = True
def should_fire(self) -> bool:
"""Check if alert should fire"""
if not self.enabled:
return False
if self.last_fired:
time_since_last = (datetime.utcnow() - self.last_fired).total_seconds()
if time_since_last < self.cooldown:
return False
return self.condition()
def fire(self) -> Alert:
"""Create alert from this rule"""
self.last_fired = datetime.utcnow()
return Alert(
id=f"{self.name}-{int(datetime.utcnow().timestamp())}",
severity=self.severity,
title=self.title_template,
message=self.message_template,
source=self.source,
metadata=self.metadata
)
class AlertManager:
"""Alert manager for handling alerts and rules"""
def __init__(self):
"""Initialize alert manager"""
self.rules: Dict[str, AlertRule] = {}
self.channels: List[AlertChannel] = []
self.active_alerts: Dict[str, Alert] = {}
self.alert_history: List[Alert] = []
self._running = False
self._task: Optional[asyncio.Task] = None
def add_rule(self, rule: AlertRule) -> None:
"""
Add alert rule
Args:
rule: Alert rule to add
"""
self.rules[rule.name] = rule
logger.info(f"Added alert rule: {rule.name}")
def remove_rule(self, name: str) -> None:
"""
Remove alert rule
Args:
name: Rule name
"""
if name in self.rules:
del self.rules[name]
logger.info(f"Removed alert rule: {name}")
def add_channel(self, channel: AlertChannel) -> None:
"""
Add alert channel
Args:
channel: Alert channel to add
"""
self.channels.append(channel)
logger.info(f"Added alert channel: {channel.__class__.__name__}")
async def check_rules(self) -> None:
"""Check all alert rules and fire if needed"""
for rule in self.rules.values():
try:
if rule.should_fire():
alert = rule.fire()
await self.send_alert(alert)
except Exception as e:
logger.error(f"Error checking rule {rule.name}: {e}")
async def send_alert(self, alert: Alert) -> None:
"""
Send alert through all channels
Args:
alert: Alert to send
"""
self.active_alerts[alert.id] = alert
self.alert_history.append(alert)
# Keep history limited
if len(self.alert_history) > 1000:
self.alert_history = self.alert_history[-1000:]
# Send through all channels
for channel in self.channels:
try:
await channel.send(alert)
except Exception as e:
logger.error(f"Failed to send alert through channel: {e}")
async def acknowledge_alert(self, alert_id: str, acknowledged_by: str) -> bool:
"""
Acknowledge an alert
Args:
alert_id: Alert ID
acknowledged_by: User acknowledging the alert
Returns:
True if acknowledged successfully
"""
if alert_id in self.active_alerts:
alert = self.active_alerts[alert_id]
alert.status = AlertStatus.ACKNOWLEDGED
alert.acknowledged_by = acknowledged_by
alert.acknowledged_at = datetime.utcnow()
logger.info(f"Alert acknowledged: {alert_id} by {acknowledged_by}")
return True
return False
async def resolve_alert(self, alert_id: str) -> bool:
"""
Resolve an alert
Args:
alert_id: Alert ID
Returns:
True if resolved successfully
"""
if alert_id in self.active_alerts:
alert = self.active_alerts[alert_id]
alert.status = AlertStatus.RESOLVED
alert.resolved_at = datetime.utcnow()
del self.active_alerts[alert_id]
logger.info(f"Alert resolved: {alert_id}")
return True
return False
def get_active_alerts(self) -> List[Alert]:
"""Get all active alerts"""
return list(self.active_alerts.values())
def get_alert_history(self, limit: int = 100) -> List[Alert]:
"""
Get alert history
Args:
limit: Maximum number of alerts to return
Returns:
List of alerts
"""
return self.alert_history[-limit:]
async def start(self) -> None:
"""Start alert manager background task"""
if self._running:
return
self._running = True
self._task = asyncio.create_task(self._run_checks())
logger.info("Alert manager started")
async def stop(self) -> None:
"""Stop alert manager background task"""
if not self._running:
return
self._running = False
if self._task:
self._task.cancel()
try:
await self._task
except asyncio.CancelledError:
pass
logger.info("Alert manager stopped")
async def _run_checks(self) -> None:
"""Background task to check alert rules"""
while self._running:
try:
await self.check_rules()
# Calculate sleep time based on minimum check interval
min_interval = min((rule.check_interval for rule in self.rules.values()), default=60)
await asyncio.sleep(min_interval)
except asyncio.CancelledError:
break
except Exception as e:
logger.error(f"Error in alert check loop: {e}")
await asyncio.sleep(60)
# Global alert manager instance
_alert_manager: Optional[AlertManager] = None
def get_alert_manager() -> AlertManager:
"""
Get global alert manager instance
Returns:
Alert manager instance
"""
global _alert_manager
if _alert_manager is None:
_alert_manager = AlertManager()
# Add default log channel
_alert_manager.add_channel(LogAlertChannel())
return _alert_manager
def setup_alerting(
webhook_url: Optional[str] = None,
webhook_headers: Optional[Dict[str, str]] = None
) -> AlertManager:
"""
Setup alerting system
Args:
webhook_url: Optional webhook URL for alerts
webhook_headers: Optional webhook headers
Returns:
Alert manager instance
"""
manager = get_alert_manager()
if webhook_url:
manager.add_channel(WebhookAlertChannel(webhook_url, webhook_headers))
return manager

294
aitbc/tracing.py Normal file
View File

@@ -0,0 +1,294 @@
"""
AITBC Distributed Tracing Module
OpenTelemetry-based distributed tracing for AITBC applications
"""
from typing import Optional, Dict, Any, Callable
from functools import wraps
from contextlib import contextmanager
import os
# OpenTelemetry imports (optional - gracefully handle if not installed)
try:
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
from opentelemetry.sdk.resources import Resource, SERVICE_NAME
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from opentelemetry.trace import Status, StatusCode
OPENTELEMETRY_AVAILABLE = True
except ImportError:
OPENTELEMETRY_AVAILABLE = False
# Global tracer instance
_tracer: Optional[object] = None
_tracer_provider: Optional[object] = None
def setup_tracing(
service_name: str,
service_version: str = "1.0.0",
exporter: str = "console",
sample_rate: float = 1.0
) -> None:
"""
Setup OpenTelemetry tracing for the service
Args:
service_name: Name of the service
service_version: Version of the service
exporter: Exporter type ('console', 'otlp', 'none')
sample_rate: Sampling rate (0.0 to 1.0)
"""
global _tracer, _tracer_provider
if not OPENTELEMETRY_AVAILABLE:
print("OpenTelemetry not available, tracing disabled")
return
# Create resource with service information
resource = Resource.create({
SERVICE_NAME: service_name,
"service.version": service_version,
"deployment.environment": os.getenv("APP_ENV", "development")
})
# Create tracer provider
_tracer_provider = TracerProvider(resource=resource)
# Configure exporter based on type
if exporter == "console":
span_processor = BatchSpanProcessor(ConsoleSpanExporter())
_tracer_provider.add_span_processor(span_processor)
elif exporter == "otlp":
try:
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
otlp_endpoint = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317")
span_processor = BatchSpanProcessor(OTLPSpanExporter(endpoint=otlp_endpoint))
_tracer_provider.add_span_processor(span_processor)
except ImportError:
print("OTLP exporter not available, falling back to console")
span_processor = BatchSpanProcessor(ConsoleSpanExporter())
_tracer_provider.add_span_processor(span_processor)
# Set global tracer provider
trace.set_tracer_provider(_tracer_provider)
# Get tracer
_tracer = trace.get_tracer(__name__)
print(f"Tracing enabled for {service_name} with {exporter} exporter")
def get_tracer() -> Optional[object]:
"""
Get the global tracer instance
Returns:
Tracer instance or None if not configured
"""
return _tracer
def instrument_fastapi(app) -> None:
"""
Instrument FastAPI application with tracing
Args:
app: FastAPI application instance
"""
if not OPENTELEMETRY_AVAILABLE:
print("OpenTelemetry not available, FastAPI instrumentation disabled")
return
try:
FastAPIInstrumentor.instrument_app(app)
print("FastAPI instrumentation enabled")
except Exception as e:
print(f"Failed to instrument FastAPI: {e}")
def instrument_httpx() -> None:
"""Instrument HTTPX client with tracing"""
if not OPENTELEMETRY_AVAILABLE:
print("OpenTelemetry not available, HTTPX instrumentation disabled")
return
try:
HTTPXClientInstrumentor().instrument()
print("HTTPX instrumentation enabled")
except Exception as e:
print(f"Failed to instrument HTTPX: {e}")
def instrument_sqlalchemy(engine) -> None:
"""
Instrument SQLAlchemy engine with tracing
Args:
engine: SQLAlchemy engine instance
"""
if not OPENTELEMETRY_AVAILABLE:
print("OpenTelemetry not available, SQLAlchemy instrumentation disabled")
return
try:
SQLAlchemyInstrumentor().instrument(engine=engine)
print("SQLAlchemy instrumentation enabled")
except Exception as e:
print(f"Failed to instrument SQLAlchemy: {e}")
@contextmanager
def trace_span(
name: str,
attributes: Optional[Dict[str, Any]] = None
):
"""
Context manager for creating a trace span
Args:
name: Span name
attributes: Span attributes
Yields:
Span object if tracing is available
"""
if not OPENTELEMETRY_AVAILABLE or _tracer is None:
yield None
return
with _tracer.start_as_current_span(name, attributes=attributes or {}) as span:
yield span
def trace_function(name: Optional[str] = None):
"""
Decorator for tracing function execution
Args:
name: Span name (defaults to function name)
Returns:
Decorated function
"""
def decorator(func: Callable) -> Callable:
if not OPENTELEMETRY_AVAILABLE or _tracer is None:
return func
span_name = name or f"{func.__module__}.{func.__name__}"
@wraps(func)
def wrapper(*args, **kwargs):
with _tracer.start_as_current_span(span_name) as span:
# Add function arguments as attributes (if small)
try:
if args and len(args) < 3:
span.set_attribute("args", str(args))
if kwargs and len(kwargs) < 3:
span.set_attribute("kwargs", str(kwargs))
except Exception:
pass
try:
result = func(*args, **kwargs)
span.set_status(Status(StatusCode.OK))
return result
except Exception as e:
span.set_status(Status(StatusCode.ERROR, str(e)))
span.record_exception(e)
raise
return wrapper
return decorator
def trace_async_function(name: Optional[str] = None):
"""
Decorator for tracing async function execution
Args:
name: Span name (defaults to function name)
Returns:
Decorated async function
"""
def decorator(func: Callable) -> Callable:
if not OPENTELEMETRY_AVAILABLE or _tracer is None:
return func
span_name = name or f"{func.__module__}.{func.__name__}"
@wraps(func)
async def wrapper(*args, **kwargs):
with _tracer.start_as_current_span(span_name) as span:
# Add function arguments as attributes (if small)
try:
if args and len(args) < 3:
span.set_attribute("args", str(args))
if kwargs and len(kwargs) < 3:
span.set_attribute("kwargs", str(kwargs))
except Exception:
pass
try:
result = await func(*args, **kwargs)
span.set_status(Status(StatusCode.OK))
return result
except Exception as e:
span.set_status(Status(StatusCode.ERROR, str(e)))
span.record_exception(e)
raise
return wrapper
return decorator
def set_span_attribute(key: str, value: Any) -> None:
"""
Set an attribute on the current span
Args:
key: Attribute key
value: Attribute value
"""
if not OPENTELEMETRY_AVAILABLE:
return
current_span = trace.get_current_span()
if current_span:
current_span.set_attribute(key, str(value))
def set_span_error(exception: Exception) -> None:
"""
Record an exception on the current span
Args:
exception: Exception to record
"""
if not OPENTELEMETRY_AVAILABLE:
return
current_span = trace.get_current_span()
if current_span:
current_span.set_status(Status(StatusCode.ERROR, str(exception)))
current_span.record_exception(exception)
def add_span_event(name: str, attributes: Optional[Dict[str, Any]] = None) -> None:
"""
Add an event to the current span
Args:
name: Event name
attributes: Event attributes
"""
if not OPENTELEMETRY_AVAILABLE:
return
current_span = trace.get_current_span()
if current_span:
current_span.add_event(name, attributes or {})

View File

@@ -10,9 +10,11 @@ from datetime import datetime, timezone
from typing import Any from typing import Any
import psutil import psutil
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends, Request
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from aitbc.rate_limiting import rate_limit
from aitbc import get_logger from aitbc import get_logger
from ..services.ai_analytics.adaptive_learning import AdaptiveLearningService from ..services.ai_analytics.adaptive_learning import AdaptiveLearningService
from ..storage import get_session from ..storage import get_session
@@ -23,7 +25,8 @@ router = APIRouter()
@router.get("/health", tags=["health"], summary="Adaptive Learning Service Health") @router.get("/health", tags=["health"], summary="Adaptive Learning Service Health")
async def adaptive_learning_health(session: Annotated[Session, Depends(get_session)]) -> dict[str, Any]: @rate_limit(rate=1000, per=60)
async def adaptive_learning_health(request: Request, session: Annotated[Session, Depends(get_session)]) -> dict[str, Any]:
""" """
Health check for Adaptive Learning Service (Port 8011) Health check for Adaptive Learning Service (Port 8011)
""" """
@@ -103,7 +106,8 @@ async def adaptive_learning_health(session: Annotated[Session, Depends(get_sessi
@router.get("/health/deep", tags=["health"], summary="Deep Adaptive Learning Service Health") @router.get("/health/deep", tags=["health"], summary="Deep Adaptive Learning Service Health")
async def adaptive_learning_deep_health(session: Annotated[Session, Depends(get_session)]) -> dict[str, Any]: @rate_limit(rate=1000, per=60)
async def adaptive_learning_deep_health(request: Request, session: Annotated[Session, Depends(get_session)]) -> dict[str, Any]:
""" """
Deep health check with learning framework validation Deep health check with learning framework validation
""" """

View File

@@ -4,12 +4,11 @@ from datetime import datetime, timezone
from typing import Annotated from typing import Annotated
from fastapi import APIRouter, Depends, Header, HTTPException, Request from fastapi import APIRouter, Depends, Header, HTTPException, Request
from slowapi import Limiter
from slowapi.util import get_remote_address
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlmodel import select from sqlmodel import select
from aitbc import get_logger from aitbc import get_logger
from aitbc.rate_limiting import rate_limit
from ..config import settings from ..config import settings
from ..deps import require_admin_key from ..deps import require_admin_key
@@ -19,12 +18,12 @@ from ..utils.cache import cached, get_cache_config
logger = get_logger(__name__) logger = get_logger(__name__)
limiter = Limiter(key_func=get_remote_address)
router = APIRouter(prefix="/admin", tags=["admin"]) router = APIRouter(prefix="/admin", tags=["admin"])
@router.get("/debug-settings", summary="Debug settings") @router.get("/debug-settings", summary="Debug settings")
async def debug_settings() -> dict: # type: ignore[arg-type] @rate_limit(rate=100, per=60)
async def debug_settings(request: Request) -> dict: # type: ignore[arg-type]
# SECURITY FIX: Mask API keys before returning to prevent clear-text exposure # SECURITY FIX: Mask API keys before returning to prevent clear-text exposure
def mask_keys(keys: list[str]) -> list[str]: def mask_keys(keys: list[str]) -> list[str]:
return [key[:8] + "..." if len(key) > 8 else "***" for key in keys] return [key[:8] + "..." if len(key) > 8 else "***" for key in keys]
@@ -38,8 +37,9 @@ async def debug_settings() -> dict: # type: ignore[arg-type]
@router.post("/debug/create-test-miner", summary="Create a test miner for debugging") @router.post("/debug/create-test-miner", summary="Create a test miner for debugging")
@rate_limit(rate=10, per=60)
async def create_test_miner( async def create_test_miner(
session: Annotated[Session, Depends(get_session)], admin_key: str = Depends(require_admin_key()) request: Request, session: Annotated[Session, Depends(get_session)], admin_key: str = Depends(require_admin_key())
) -> dict[str, str]: # type: ignore[arg-type] ) -> dict[str, str]: # type: ignore[arg-type]
"""Create a test miner for debugging marketplace sync""" """Create a test miner for debugging marketplace sync"""
try: try:
@@ -101,7 +101,8 @@ async def create_test_miner(
@router.get("/test-key", summary="Test API key validation") @router.get("/test-key", summary="Test API key validation")
async def test_key(api_key: str = Header(default=None, alias="X-Api-Key")) -> dict[str, str]: # type: ignore[arg-type] @rate_limit(rate=100, per=60)
async def test_key(request: Request, api_key: str = Header(default=None, alias="X-Api-Key")) -> dict[str, str]: # type: ignore[arg-type]
masked_key = api_key[:8] + "..." if api_key else "None" masked_key = api_key[:8] + "..." if api_key else "None"
logger.debug(f"Received API key: {masked_key}") logger.debug(f"Received API key: {masked_key}")
logger.debug(f"Allowed admin keys count: {len(settings.admin_api_keys)}") logger.debug(f"Allowed admin keys count: {len(settings.admin_api_keys)}")
@@ -115,7 +116,7 @@ async def test_key(api_key: str = Header(default=None, alias="X-Api-Key")) -> di
@router.get("/stats", summary="Get coordinator stats") @router.get("/stats", summary="Get coordinator stats")
@limiter.limit(lambda: settings.rate_limit_admin_stats) @rate_limit(rate=100, per=60)
@cached(**get_cache_config("job_list")) # Cache admin stats for 1 minute @cached(**get_cache_config("job_list")) # Cache admin stats for 1 minute
async def get_stats( async def get_stats(
request: Request, session: Annotated[Session, Depends(get_session)], api_key: str = Header(default=None, alias="X-Api-Key") request: Request, session: Annotated[Session, Depends(get_session)], api_key: str = Header(default=None, alias="X-Api-Key")
@@ -151,7 +152,8 @@ async def get_stats(
@router.get("/jobs", summary="List jobs") @router.get("/jobs", summary="List jobs")
async def list_jobs(session: Annotated[Session, Depends(get_session)], admin_key: str = Depends(require_admin_key())) -> dict[str, list[dict]]: # type: ignore[arg-type] @rate_limit(rate=100, per=60)
async def list_jobs(request: Request, session: Annotated[Session, Depends(get_session)], admin_key: str = Depends(require_admin_key())) -> dict[str, list[dict]]: # type: ignore[arg-type]
from ..domain import Job from ..domain import Job
jobs = session.execute(select(Job).order_by(Job.requested_at.desc()).limit(100)).all() jobs = session.execute(select(Job).order_by(Job.requested_at.desc()).limit(100)).all()
@@ -170,7 +172,8 @@ async def list_jobs(session: Annotated[Session, Depends(get_session)], admin_key
@router.get("/miners", summary="List miners") @router.get("/miners", summary="List miners")
async def list_miners(session: Annotated[Session, Depends(get_session)], admin_key: str = Depends(require_admin_key())) -> dict[str, list[dict]]: # type: ignore[arg-type] @rate_limit(rate=100, per=60)
async def list_miners(request: Request, session: Annotated[Session, Depends(get_session)], admin_key: str = Depends(require_admin_key())) -> dict[str, list[dict]]: # type: ignore[arg-type]
from sqlmodel import select from sqlmodel import select
from ..domain import Miner from ..domain import Miner
@@ -195,6 +198,7 @@ async def list_miners(session: Annotated[Session, Depends(get_session)], admin_k
@router.get("/status", summary="Get system status", response_model=None) @router.get("/status", summary="Get system status", response_model=None)
@rate_limit(rate=100, per=60)
async def get_system_status( async def get_system_status(
request: Request, session: Annotated[Session, Depends(get_session)], admin_key: str = Depends(require_admin_key()) request: Request, session: Annotated[Session, Depends(get_session)], admin_key: str = Depends(require_admin_key())
) -> dict[str, any]: # type: ignore[arg-type] ) -> dict[str, any]: # type: ignore[arg-type]
@@ -263,7 +267,8 @@ async def get_system_status(
# Agent endpoints temporarily added to admin router # Agent endpoints temporarily added to admin router
@router.post("/agents/networks", response_model=dict, status_code=201) @router.post("/agents/networks", response_model=dict, status_code=201)
async def create_agent_network(network_data: dict) -> dict: @rate_limit(rate=20, per=60)
async def create_agent_network(request: Request, network_data: dict) -> dict:
"""Create a new agent network for collaborative processing""" """Create a new agent network for collaborative processing"""
try: try:
@@ -300,7 +305,8 @@ async def create_agent_network(network_data: dict) -> dict:
@router.get("/agents/executions/{execution_id}/receipt") @router.get("/agents/executions/{execution_id}/receipt")
async def get_execution_receipt(execution_id: str) -> dict: @rate_limit(rate=100, per=60)
async def get_execution_receipt(request: Request, execution_id: str) -> dict:
"""Get verifiable receipt for completed execution""" """Get verifiable receipt for completed execution"""
try: try:

View File

@@ -5,9 +5,10 @@ Agent Integration and Deployment API Router for Verifiable AI Agent Orchestratio
Provides REST API endpoints for production deployment and integration management Provides REST API endpoints for production deployment and integration management
""" """
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException, Request
from aitbc import get_logger from aitbc import get_logger
from aitbc.rate_limiting import rate_limit
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -31,7 +32,9 @@ router = APIRouter(prefix="/agents/integration", tags=["Agent Integration"])
@router.post("/deployments/config", response_model=AgentDeploymentConfig) @router.post("/deployments/config", response_model=AgentDeploymentConfig)
@rate_limit(rate=20, per=60)
async def create_deployment_config( async def create_deployment_config(
request: Request,
workflow_id: str, workflow_id: str,
deployment_name: str, deployment_name: str,
deployment_config: dict, deployment_config: dict,
@@ -65,7 +68,9 @@ async def create_deployment_config(
@router.get("/deployments/configs", response_model=list[AgentDeploymentConfig]) @router.get("/deployments/configs", response_model=list[AgentDeploymentConfig])
@rate_limit(rate=200, per=60)
async def list_deployment_configs( async def list_deployment_configs(
request: Request,
workflow_id: str | None = None, workflow_id: str | None = None,
status: DeploymentStatus | None = None, status: DeploymentStatus | None = None,
session: Session = Depends(Annotated[Session, Depends(get_session)]), session: Session = Depends(Annotated[Session, Depends(get_session)]),
@@ -99,7 +104,9 @@ async def list_deployment_configs(
@router.get("/deployments/configs/{config_id}", response_model=AgentDeploymentConfig) @router.get("/deployments/configs/{config_id}", response_model=AgentDeploymentConfig)
@rate_limit(rate=200, per=60)
async def get_deployment_config( async def get_deployment_config(
request: Request,
config_id: str, config_id: str,
session: Session = Depends(Annotated[Session, Depends(get_session)]), session: Session = Depends(Annotated[Session, Depends(get_session)]),
current_user: str = Depends(require_admin_key()), current_user: str = Depends(require_admin_key()),
@@ -126,7 +133,9 @@ async def get_deployment_config(
@router.post("/deployments/{config_id}/deploy") @router.post("/deployments/{config_id}/deploy")
@rate_limit(rate=20, per=60)
async def deploy_workflow( async def deploy_workflow(
request: Request,
config_id: str, config_id: str,
target_environment: str = "production", target_environment: str = "production",
session: Session = Depends(Annotated[Session, Depends(get_session)]), session: Session = Depends(Annotated[Session, Depends(get_session)]),
@@ -160,7 +169,9 @@ async def deploy_workflow(
@router.get("/deployments/{config_id}/health") @router.get("/deployments/{config_id}/health")
@rate_limit(rate=200, per=60)
async def get_deployment_health( async def get_deployment_health(
request: Request,
config_id: str, config_id: str,
session: Session = Depends(Annotated[Session, Depends(get_session)]), session: Session = Depends(Annotated[Session, Depends(get_session)]),
current_user: str = Depends(require_admin_key()), current_user: str = Depends(require_admin_key()),
@@ -190,7 +201,9 @@ async def get_deployment_health(
@router.post("/deployments/{config_id}/scale") @router.post("/deployments/{config_id}/scale")
@rate_limit(rate=20, per=60)
async def scale_deployment( async def scale_deployment(
request: Request,
config_id: str, config_id: str,
target_instances: int, target_instances: int,
session: Session = Depends(Annotated[Session, Depends(get_session)]), session: Session = Depends(Annotated[Session, Depends(get_session)]),
@@ -224,7 +237,9 @@ async def scale_deployment(
@router.post("/deployments/{config_id}/rollback") @router.post("/deployments/{config_id}/rollback")
@rate_limit(rate=20, per=60)
async def rollback_deployment( async def rollback_deployment(
request: Request,
config_id: str, config_id: str,
session: Session = Depends(Annotated[Session, Depends(get_session)]), session: Session = Depends(Annotated[Session, Depends(get_session)]),
current_user: str = Depends(require_admin_key()), current_user: str = Depends(require_admin_key()),
@@ -255,7 +270,9 @@ async def rollback_deployment(
@router.get("/deployments/instances", response_model=list[AgentDeploymentInstance]) @router.get("/deployments/instances", response_model=list[AgentDeploymentInstance])
@rate_limit(rate=200, per=60)
async def list_deployment_instances( async def list_deployment_instances(
request: Request,
deployment_id: str | None = None, deployment_id: str | None = None,
environment: str | None = None, environment: str | None = None,
status: DeploymentStatus | None = None, status: DeploymentStatus | None = None,
@@ -295,7 +312,9 @@ async def list_deployment_instances(
@router.get("/deployments/instances/{instance_id}", response_model=AgentDeploymentInstance) @router.get("/deployments/instances/{instance_id}", response_model=AgentDeploymentInstance)
@rate_limit(rate=200, per=60)
async def get_deployment_instance( async def get_deployment_instance(
request: Request,
instance_id: str, instance_id: str,
session: Session = Depends(Annotated[Session, Depends(get_session)]), session: Session = Depends(Annotated[Session, Depends(get_session)]),
current_user: str = Depends(require_admin_key()), current_user: str = Depends(require_admin_key()),
@@ -326,7 +345,9 @@ async def get_deployment_instance(
@router.post("/integrations/zk/{execution_id}") @router.post("/integrations/zk/{execution_id}")
@rate_limit(rate=20, per=60)
async def integrate_with_zk_system( async def integrate_with_zk_system(
request: Request,
execution_id: str, execution_id: str,
verification_level: VerificationLevel = VerificationLevel.BASIC, verification_level: VerificationLevel = VerificationLevel.BASIC,
session: Session = Depends(Annotated[Session, Depends(get_session)]), session: Session = Depends(Annotated[Session, Depends(get_session)]),
@@ -360,7 +381,9 @@ async def integrate_with_zk_system(
@router.get("/metrics/deployments/{deployment_id}") @router.get("/metrics/deployments/{deployment_id}")
@rate_limit(rate=200, per=60)
async def get_deployment_metrics( async def get_deployment_metrics(
request: Request,
deployment_id: str, deployment_id: str,
time_range: str = "1h", time_range: str = "1h",
session: Session = Depends(Annotated[Session, Depends(get_session)]), session: Session = Depends(Annotated[Session, Depends(get_session)]),
@@ -391,7 +414,9 @@ async def get_deployment_metrics(
@router.post("/production/deploy") @router.post("/production/deploy")
@rate_limit(rate=20, per=60)
async def deploy_to_production( async def deploy_to_production(
request: Request,
workflow_id: str, workflow_id: str,
deployment_config: dict, deployment_config: dict,
integration_config: dict | None = None, integration_config: dict | None = None,
@@ -425,8 +450,9 @@ async def deploy_to_production(
@router.get("/production/dashboard") @router.get("/production/dashboard")
@rate_limit(rate=200, per=60)
async def get_production_dashboard( async def get_production_dashboard(
session: Session = Depends(Annotated[Session, Depends(get_session)]), current_user: str = Depends(require_admin_key()) request: Request, session: Session = Depends(Annotated[Session, Depends(get_session)]), current_user: str = Depends(require_admin_key())
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Get comprehensive production dashboard data""" """Get comprehensive production dashboard data"""
@@ -479,8 +505,9 @@ async def get_production_dashboard(
@router.get("/production/health") @router.get("/production/health")
@rate_limit(rate=1000, per=60)
async def get_production_health( async def get_production_health(
session: Session = Depends(Annotated[Session, Depends(get_session)]), current_user: str = Depends(require_admin_key()) request: Request, session: Session = Depends(Annotated[Session, Depends(get_session)]), current_user: str = Depends(require_admin_key())
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Get overall production health status""" """Get overall production health status"""
@@ -549,7 +576,9 @@ async def get_production_health(
@router.get("/production/alerts") @router.get("/production/alerts")
@rate_limit(rate=200, per=60)
async def get_production_alerts( async def get_production_alerts(
request: Request,
severity: str | None = None, severity: str | None = None,
limit: int = 50, limit: int = 50,
current_user: str = Depends(require_admin_key()), current_user: str = Depends(require_admin_key()),

View File

@@ -4,11 +4,10 @@ from datetime import datetime, timezone
from typing import Annotated from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi import APIRouter, Depends, HTTPException, Request, status
from slowapi import Limiter
from slowapi.util import get_remote_address
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from aitbc import get_logger, AITBCHTTPClient, NetworkError from aitbc import get_logger, AITBCHTTPClient, NetworkError
from aitbc.rate_limiting import rate_limit
from ..config import settings from ..config import settings
from ..custom_types import JobState from ..custom_types import JobState
@@ -21,12 +20,11 @@ from ..utils.cache import cached, get_cache_config
logger = get_logger(__name__) logger = get_logger(__name__)
limiter = Limiter(key_func=get_remote_address)
router = APIRouter(tags=["client"]) router = APIRouter(tags=["client"])
@router.post("/jobs", response_model=JobView, status_code=status.HTTP_201_CREATED, summary="Submit a job") @router.post("/jobs", response_model=JobView, status_code=status.HTTP_201_CREATED, summary="Submit a job")
@limiter.limit(lambda: settings.rate_limit_jobs_submit) @rate_limit(rate=50, per=60)
async def submit_job( async def submit_job(
req: JobCreate, req: JobCreate,
request: Request, request: Request,
@@ -55,8 +53,9 @@ async def submit_job(
@router.get("/jobs/{job_id}", response_model=JobView, summary="Get job status") @router.get("/jobs/{job_id}", response_model=JobView, summary="Get job status")
@rate_limit(rate=200, per=60)
async def get_job( async def get_job(
job_id: str, request: Request, job_id: str,
session: Annotated[Session, Depends(get_session)], session: Annotated[Session, Depends(get_session)],
client_id: str = Depends(require_client_key()), client_id: str = Depends(require_client_key()),
) -> JobView: # type: ignore[arg-type] ) -> JobView: # type: ignore[arg-type]
@@ -69,8 +68,9 @@ async def get_job(
@router.get("/jobs/{job_id}/result", response_model=JobResult, summary="Get job result") @router.get("/jobs/{job_id}/result", response_model=JobResult, summary="Get job result")
@rate_limit(rate=200, per=60)
async def get_job_result( async def get_job_result(
job_id: str, request: Request, job_id: str,
session: Annotated[Session, Depends(get_session)], session: Annotated[Session, Depends(get_session)],
client_id: str = Depends(require_client_key()), client_id: str = Depends(require_client_key()),
) -> JobResult: # type: ignore[arg-type] ) -> JobResult: # type: ignore[arg-type]
@@ -88,8 +88,9 @@ async def get_job_result(
@router.post("/jobs/{job_id}/cancel", response_model=JobView, summary="Cancel job") @router.post("/jobs/{job_id}/cancel", response_model=JobView, summary="Cancel job")
@rate_limit(rate=50, per=60)
async def cancel_job( async def cancel_job(
job_id: str, request: Request, job_id: str,
session: Annotated[Session, Depends(get_session)], session: Annotated[Session, Depends(get_session)],
client_id: str = Depends(require_client_key()), client_id: str = Depends(require_client_key()),
) -> JobView: # type: ignore[arg-type] ) -> JobView: # type: ignore[arg-type]
@@ -107,8 +108,9 @@ async def cancel_job(
@router.get("/jobs/{job_id}/receipt", summary="Get latest signed receipt") @router.get("/jobs/{job_id}/receipt", summary="Get latest signed receipt")
@rate_limit(rate=200, per=60)
async def get_job_receipt( async def get_job_receipt(
job_id: str, request: Request, job_id: str,
session: Annotated[Session, Depends(get_session)], session: Annotated[Session, Depends(get_session)],
client_id: str = Depends(require_client_key()), client_id: str = Depends(require_client_key()),
) -> dict: # type: ignore[arg-type] ) -> dict: # type: ignore[arg-type]
@@ -123,8 +125,9 @@ async def get_job_receipt(
@router.get("/jobs/{job_id}/receipts", summary="List signed receipts") @router.get("/jobs/{job_id}/receipts", summary="List signed receipts")
@rate_limit(rate=200, per=60)
async def list_job_receipts( async def list_job_receipts(
job_id: str, request: Request, job_id: str,
session: Annotated[Session, Depends(get_session)], session: Annotated[Session, Depends(get_session)],
client_id: str = Depends(require_client_key()), client_id: str = Depends(require_client_key()),
) -> dict: # type: ignore[arg-type] ) -> dict: # type: ignore[arg-type]
@@ -134,6 +137,7 @@ async def list_job_receipts(
@router.get("/jobs", summary="List jobs with filtering") @router.get("/jobs", summary="List jobs with filtering")
@rate_limit(rate=200, per=60)
@cached(**get_cache_config("job_list")) # Cache job list for 30 seconds @cached(**get_cache_config("job_list")) # Cache job list for 30 seconds
async def list_jobs( async def list_jobs(
request: Request, request: Request,
@@ -164,6 +168,7 @@ async def list_jobs(
@router.get("/jobs/history", summary="Get job history") @router.get("/jobs/history", summary="Get job history")
@rate_limit(rate=200, per=60)
@cached(**get_cache_config("job_list")) # Cache job history for 30 seconds @cached(**get_cache_config("job_list")) # Cache job history for 30 seconds
async def get_job_history( async def get_job_history(
request: Request, request: Request,
@@ -216,6 +221,7 @@ async def get_job_history(
@router.get("/blocks", summary="Get blockchain blocks") @router.get("/blocks", summary="Get blockchain blocks")
@rate_limit(rate=200, per=60)
async def get_blocks( async def get_blocks(
request: Request, request: Request,
session: Annotated[Session, Depends(get_session)], session: Annotated[Session, Depends(get_session)],
@@ -252,7 +258,8 @@ async def get_blocks(
# Temporary agent endpoints added to client router until agent router issue is resolved # Temporary agent endpoints added to client router until agent router issue is resolved
@router.post("/agents/networks", response_model=dict, status_code=201) @router.post("/agents/networks", response_model=dict, status_code=201)
async def create_agent_network(network_data: dict) -> dict: @rate_limit(rate=20, per=60)
async def create_agent_network(request: Request, network_data: dict) -> dict:
"""Create a new agent network for collaborative processing""" """Create a new agent network for collaborative processing"""
try: try:
@@ -286,7 +293,8 @@ async def create_agent_network(network_data: dict) -> dict:
@router.get("/agents/executions/{execution_id}/receipt") @router.get("/agents/executions/{execution_id}/receipt")
async def get_execution_receipt(execution_id: str) -> dict: @rate_limit(rate=200, per=60)
async def get_execution_receipt(request: Request, execution_id: str) -> dict:
"""Get verifiable receipt for completed execution""" """Get verifiable receipt for completed execution"""
try: try:

View File

@@ -9,9 +9,11 @@ REST API for managing hermes developer profiles, SDKs, solutions, and hackathons
from typing import Any from typing import Any
from fastapi import APIRouter, Body, Depends, HTTPException, Query from fastapi import APIRouter, Body, Depends, HTTPException, Query, Request
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from aitbc.rate_limiting import rate_limit
from aitbc import get_logger from aitbc import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -84,7 +86,8 @@ class HackathonCreateRequest(BaseModel):
# Endpoints - Developer Ecosystem # Endpoints - Developer Ecosystem
@router.post("/developers", response_model=DeveloperProfile) @router.post("/developers", response_model=DeveloperProfile)
async def create_developer_profile(request: DeveloperProfileCreate, session: Annotated[Session, Depends(get_session)]) -> DeveloperProfile: @rate_limit(rate=10, per=60)
async def create_developer_profile(request: DeveloperProfileCreate, request_http: Request, session: Annotated[Session, Depends(get_session)]) -> DeveloperProfile:
"""Register a new developer in the hermes ecosystem""" """Register a new developer in the hermes ecosystem"""
service = DeveloperEcosystemService(session) service = DeveloperEcosystemService(session)
try: try:
@@ -98,7 +101,8 @@ async def create_developer_profile(request: DeveloperProfileCreate, session: Ann
@router.get("/developers/{developer_id}", response_model=DeveloperProfile) @router.get("/developers/{developer_id}", response_model=DeveloperProfile)
async def get_developer_profile(developer_id: str, session: Annotated[Session, Depends(get_session)]) -> DeveloperProfile: @rate_limit(rate=100, per=60)
async def get_developer_profile(developer_id: str, request: Request, session: Annotated[Session, Depends(get_session)]) -> DeveloperProfile:
"""Get a developer's profile and reputation""" """Get a developer's profile and reputation"""
service = DeveloperEcosystemService(session) service = DeveloperEcosystemService(session)
profile = await service.get_developer_profile(developer_id) profile = await service.get_developer_profile(developer_id)
@@ -108,7 +112,8 @@ async def get_developer_profile(developer_id: str, session: Annotated[Session, D
@router.get("/sdk/latest") @router.get("/sdk/latest")
async def get_latest_sdk(session: Annotated[Session, Depends(get_session)]) -> dict[str, Any]: @rate_limit(rate=100, per=60)
async def get_latest_sdk(request: Request, session: Annotated[Session, Depends(get_session)]) -> dict[str, Any]:
"""Get information about the latest hermes SDK releases""" """Get information about the latest hermes SDK releases"""
service = DeveloperEcosystemService(session) service = DeveloperEcosystemService(session)
return await service.get_sdk_release_info() return await service.get_sdk_release_info()
@@ -116,7 +121,8 @@ async def get_latest_sdk(session: Annotated[Session, Depends(get_session)]) -> d
# Endpoints - Marketplace Solutions # Endpoints - Marketplace Solutions
@router.post("/solutions/publish", response_model=AgentSolution) @router.post("/solutions/publish", response_model=AgentSolution)
async def publish_solution(request: SolutionPublishRequest, session: Annotated[Session, Depends(get_session)]) -> AgentSolution: @rate_limit(rate=10, per=60)
async def publish_solution(request: SolutionPublishRequest, request_http: Request, session: Annotated[Session, Depends(get_session)]) -> AgentSolution:
"""Publish a new third-party agent solution to the marketplace""" """Publish a new third-party agent solution to the marketplace"""
service = ThirdPartySolutionService(session) service = ThirdPartySolutionService(session)
try: try:
@@ -128,9 +134,11 @@ async def publish_solution(request: SolutionPublishRequest, session: Annotated[S
@router.get("/solutions", response_model=list[AgentSolution]) @router.get("/solutions", response_model=list[AgentSolution])
@rate_limit(rate=100, per=60)
async def list_solutions( async def list_solutions(
category: str | None = None, category: str | None = None,
limit: int = 50, limit: int = 50,
request: Request,
session: Annotated[Session, Depends(get_session)] = Depends(get_session), session: Annotated[Session, Depends(get_session)] = Depends(get_session),
) -> list[AgentSolution]: ) -> list[AgentSolution]:
"""List available third-party agent solutions""" """List available third-party agent solutions"""
@@ -139,8 +147,9 @@ async def list_solutions(
@router.post("/solutions/{solution_id}/purchase") @router.post("/solutions/{solution_id}/purchase")
@rate_limit(rate=20, per=60)
async def purchase_solution( async def purchase_solution(
solution_id: str, session: Annotated[Session, Depends(get_session)], buyer_id: str = Body(embed=True) solution_id: str, request: Request, session: Annotated[Session, Depends(get_session)], buyer_id: str = Body(embed=True)
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Purchase or install a third-party solution""" """Purchase or install a third-party solution"""
service = ThirdPartySolutionService(session) service = ThirdPartySolutionService(session)
@@ -155,9 +164,11 @@ async def purchase_solution(
# Endpoints - Innovation Labs # Endpoints - Innovation Labs
@router.post("/labs/propose", response_model=InnovationLab) @router.post("/labs/propose", response_model=InnovationLab)
@rate_limit(rate=10, per=60)
async def propose_innovation_lab( async def propose_innovation_lab(
researcher_id: str = Query(...), researcher_id: str = Query(...),
request: LabProposalRequest = Body(...), request: LabProposalRequest = Body(...),
request_http: Request,
session: Annotated[Session, Depends(get_session)] = Depends(get_session), session: Annotated[Session, Depends(get_session)] = Depends(get_session),
) -> InnovationLab: ) -> InnovationLab:
"""Propose a new agent innovation lab or research program""" """Propose a new agent innovation lab or research program"""
@@ -170,8 +181,9 @@ async def propose_innovation_lab(
@router.post("/labs/{lab_id}/join") @router.post("/labs/{lab_id}/join")
@rate_limit(rate=20, per=60)
async def join_innovation_lab( async def join_innovation_lab(
lab_id: str, session: Annotated[Session, Depends(get_session)], developer_id: str = Body(embed=True) lab_id: str, request: Request, session: Annotated[Session, Depends(get_session)], developer_id: str = Body(embed=True)
) -> InnovationLab: ) -> InnovationLab:
"""Join an active innovation lab""" """Join an active innovation lab"""
service = InnovationLabService(session) service = InnovationLabService(session)
@@ -183,8 +195,9 @@ async def join_innovation_lab(
@router.post("/labs/{lab_id}/fund") @router.post("/labs/{lab_id}/fund")
@rate_limit(rate=20, per=60)
async def fund_innovation_lab( async def fund_innovation_lab(
lab_id: str, session: Annotated[Session, Depends(get_session)], amount: float = Body(embed=True) lab_id: str, request: Request, session: Annotated[Session, Depends(get_session)], amount: float = Body(embed=True)
) -> InnovationLab: ) -> InnovationLab:
"""Provide funding to a proposed innovation lab""" """Provide funding to a proposed innovation lab"""
service = InnovationLabService(session) service = InnovationLabService(session)
@@ -197,9 +210,11 @@ async def fund_innovation_lab(
# Endpoints - Community Platform # Endpoints - Community Platform
@router.post("/platform/posts", response_model=CommunityPost) @router.post("/platform/posts", response_model=CommunityPost)
@rate_limit(rate=20, per=60)
async def create_community_post( async def create_community_post(
author_id: str = Query(...), author_id: str = Query(...),
request: PostCreateRequest = Body(...), request: PostCreateRequest = Body(...),
request_http: Request,
session: Annotated[Session, Depends(get_session)] = Depends(get_session), session: Annotated[Session, Depends(get_session)] = Depends(get_session),
) -> CommunityPost: ) -> CommunityPost:
"""Create a new post in the community forum""" """Create a new post in the community forum"""
@@ -212,9 +227,11 @@ async def create_community_post(
@router.get("/platform/feed", response_model=list[CommunityPost]) @router.get("/platform/feed", response_model=list[CommunityPost])
@rate_limit(rate=100, per=60)
async def get_community_feed( async def get_community_feed(
category: str | None = None, category: str | None = None,
limit: int = 20, limit: int = 20,
request: Request,
session: Annotated[Session, Depends(get_session)] = Depends(get_session), session: Annotated[Session, Depends(get_session)] = Depends(get_session),
) -> list[CommunityPost]: ) -> list[CommunityPost]:
"""Get the latest community posts and discussions""" """Get the latest community posts and discussions"""
@@ -223,7 +240,8 @@ async def get_community_feed(
@router.post("/platform/posts/{post_id}/upvote") @router.post("/platform/posts/{post_id}/upvote")
async def upvote_community_post(post_id: str, session: Annotated[Session, Depends(get_session)]) -> CommunityPost: @rate_limit(rate=50, per=60)
async def upvote_community_post(post_id: str, request: Request, session: Annotated[Session, Depends(get_session)]) -> CommunityPost:
"""Upvote a community post (rewards author reputation)""" """Upvote a community post (rewards author reputation)"""
service = CommunityPlatformService(session) service = CommunityPlatformService(session)
try: try:
@@ -235,9 +253,11 @@ async def upvote_community_post(post_id: str, session: Annotated[Session, Depend
# Endpoints - Hackathons # Endpoints - Hackathons
@router.post("/hackathons/create", response_model=Hackathon) @router.post("/hackathons/create", response_model=Hackathon)
@rate_limit(rate=10, per=60)
async def create_hackathon( async def create_hackathon(
organizer_id: str = Query(...), organizer_id: str = Query(...),
request: HackathonCreateRequest = Body(...), request: HackathonCreateRequest = Body(...),
request_http: Request,
session: Annotated[Session, Depends(get_session)] = Depends(get_session), session: Annotated[Session, Depends(get_session)] = Depends(get_session),
) -> Hackathon: ) -> Hackathon:
"""Create a new agent innovation hackathon (requires high reputation)""" """Create a new agent innovation hackathon (requires high reputation)"""
@@ -252,8 +272,9 @@ async def create_hackathon(
@router.post("/hackathons/{hackathon_id}/register") @router.post("/hackathons/{hackathon_id}/register")
@rate_limit(rate=20, per=60)
async def register_for_hackathon( async def register_for_hackathon(
hackathon_id: str, session: Annotated[Session, Depends(get_session)], developer_id: str = Body(embed=True) hackathon_id: str, request: Request, session: Annotated[Session, Depends(get_session)], developer_id: str = Body(embed=True)
) -> Hackathon: ) -> Hackathon:
"""Register for an upcoming or ongoing hackathon""" """Register for an upcoming or ongoing hackathon"""
service = CommunityPlatformService(session) service = CommunityPlatformService(session)

View File

@@ -7,9 +7,11 @@ from datetime import datetime, timezone
from typing import Any from typing import Any
from uuid import uuid4 from uuid import uuid4
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query, Request
from sqlmodel import Session from sqlmodel import Session
from aitbc.rate_limiting import rate_limit
from ..agent_identity.manager import AgentIdentityManager from ..agent_identity.manager import AgentIdentityManager
from ..agent_identity.wallet_adapter_enhanced import ( from ..agent_identity.wallet_adapter_enhanced import (
SecurityLevel, SecurityLevel,
@@ -45,7 +47,9 @@ def get_reputation_engine(session: Session = Depends(get_session)) -> CrossChain
# Enhanced Wallet Adapter Endpoints # Enhanced Wallet Adapter Endpoints
@router.post("/wallets/create", response_model=dict[str, Any]) @router.post("/wallets/create", response_model=dict[str, Any])
@rate_limit(rate=20, per=60)
async def create_enhanced_wallet( async def create_enhanced_wallet(
request: Request,
owner_address: str, owner_address: str,
chain_id: int, chain_id: int,
security_config: dict[str, Any], security_config: dict[str, Any],
@@ -87,7 +91,9 @@ async def create_enhanced_wallet(
@router.get("/wallets/{wallet_address}/balance", response_model=dict[str, Any]) @router.get("/wallets/{wallet_address}/balance", response_model=dict[str, Any])
@rate_limit(rate=200, per=60)
async def get_wallet_balance( async def get_wallet_balance(
request: Request,
wallet_address: str, chain_id: int, token_address: str | None = Query(None), session: Session = Depends(get_session) wallet_address: str, chain_id: int, token_address: str | None = Query(None), session: Session = Depends(get_session)
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Get wallet balance with multi-token support""" """Get wallet balance with multi-token support"""
@@ -110,7 +116,9 @@ async def get_wallet_balance(
@router.post("/wallets/{wallet_address}/transactions", response_model=dict[str, Any]) @router.post("/wallets/{wallet_address}/transactions", response_model=dict[str, Any])
@rate_limit(rate=50, per=60)
async def execute_wallet_transaction( async def execute_wallet_transaction(
request: Request,
wallet_address: str, wallet_address: str,
chain_id: int, chain_id: int,
to_address: str, to_address: str,
@@ -149,7 +157,9 @@ async def execute_wallet_transaction(
@router.get("/wallets/{wallet_address}/transactions", response_model=list[dict[str, Any]]) @router.get("/wallets/{wallet_address}/transactions", response_model=list[dict[str, Any]])
@rate_limit(rate=200, per=60)
async def get_wallet_transaction_history( async def get_wallet_transaction_history(
request: Request,
wallet_address: str, wallet_address: str,
chain_id: int, chain_id: int,
limit: int = Query(100, ge=1, le=1000), limit: int = Query(100, ge=1, le=1000),
@@ -178,7 +188,9 @@ async def get_wallet_transaction_history(
@router.post("/wallets/{wallet_address}/sign", response_model=dict[str, Any]) @router.post("/wallets/{wallet_address}/sign", response_model=dict[str, Any])
@rate_limit(rate=50, per=60)
async def sign_message( async def sign_message(
request: Request,
wallet_address: str, chain_id: int, message: str, session: Session = Depends(get_session) wallet_address: str, chain_id: int, message: str, session: Session = Depends(get_session)
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Sign a message with wallet""" """Sign a message with wallet"""
@@ -200,7 +212,9 @@ async def sign_message(
@router.post("/wallets/verify-signature", response_model=dict[str, Any]) @router.post("/wallets/verify-signature", response_model=dict[str, Any])
@rate_limit(rate=200, per=60)
async def verify_signature( async def verify_signature(
request: Request,
message: str, signature: str, address: str, chain_id: int, session: Session = Depends(get_session) message: str, signature: str, address: str, chain_id: int, session: Session = Depends(get_session)
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Verify a message signature""" """Verify a message signature"""
@@ -226,7 +240,9 @@ async def verify_signature(
# Cross-Chain Bridge Endpoints # Cross-Chain Bridge Endpoints
@router.post("/bridge/create-request", response_model=dict[str, Any]) @router.post("/bridge/create-request", response_model=dict[str, Any])
@rate_limit(rate=20, per=60)
async def create_bridge_request( async def create_bridge_request(
request: Request,
user_address: str, user_address: str,
source_chain_id: int, source_chain_id: int,
target_chain_id: int, target_chain_id: int,
@@ -268,7 +284,8 @@ async def create_bridge_request(
@router.get("/bridge/request/{bridge_request_id}", response_model=dict[str, Any]) @router.get("/bridge/request/{bridge_request_id}", response_model=dict[str, Any])
async def get_bridge_request_status(bridge_request_id: str, session: Session = Depends(get_session)) -> dict[str, Any]: @rate_limit(rate=200, per=60)
async def get_bridge_request_status(request: Request, bridge_request_id: str, session: Session = Depends(get_session)) -> dict[str, Any]:
"""Get status of a bridge request""" """Get status of a bridge request"""
try: try:
@@ -285,7 +302,9 @@ async def get_bridge_request_status(bridge_request_id: str, session: Session = D
@router.post("/bridge/request/{bridge_request_id}/cancel", response_model=dict[str, Any]) @router.post("/bridge/request/{bridge_request_id}/cancel", response_model=dict[str, Any])
@rate_limit(rate=20, per=60)
async def cancel_bridge_request( async def cancel_bridge_request(
request: Request,
bridge_request_id: str, reason: str, session: Session = Depends(get_session) bridge_request_id: str, reason: str, session: Session = Depends(get_session)
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Cancel a bridge request""" """Cancel a bridge request"""
@@ -304,7 +323,9 @@ async def cancel_bridge_request(
@router.get("/bridge/statistics", response_model=dict[str, Any]) @router.get("/bridge/statistics", response_model=dict[str, Any])
@rate_limit(rate=200, per=60)
async def get_bridge_statistics( async def get_bridge_statistics(
request: Request,
time_period_hours: int = Query(24, ge=1, le=8760), session: Session = Depends(get_session) time_period_hours: int = Query(24, ge=1, le=8760), session: Session = Depends(get_session)
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Get bridge statistics""" """Get bridge statistics"""
@@ -323,7 +344,8 @@ async def get_bridge_statistics(
@router.get("/bridge/liquidity-pools", response_model=list[dict[str, Any]]) @router.get("/bridge/liquidity-pools", response_model=list[dict[str, Any]])
async def get_liquidity_pools(session: Session = Depends(get_session)) -> list[dict[str, Any]]: @rate_limit(rate=200, per=60)
async def get_liquidity_pools(request: Request, session: Session = Depends(get_session)) -> list[dict[str, Any]]:
"""Get all liquidity pool information""" """Get all liquidity pool information"""
try: try:
@@ -341,7 +363,9 @@ async def get_liquidity_pools(session: Session = Depends(get_session)) -> list[d
# Multi-Chain Transaction Manager Endpoints # Multi-Chain Transaction Manager Endpoints
@router.post("/transactions/submit", response_model=dict[str, Any]) @router.post("/transactions/submit", response_model=dict[str, Any])
@rate_limit(rate=50, per=60)
async def submit_transaction( async def submit_transaction(
request: Request,
user_id: str, user_id: str,
chain_id: int, chain_id: int,
transaction_type: TransactionType, transaction_type: TransactionType,
@@ -395,7 +419,8 @@ async def submit_transaction(
@router.get("/transactions/{transaction_id}", response_model=dict[str, Any]) @router.get("/transactions/{transaction_id}", response_model=dict[str, Any])
async def get_transaction_status(transaction_id: str, session: Session = Depends(get_session)) -> dict[str, Any]: @rate_limit(rate=200, per=60)
async def get_transaction_status(request: Request, transaction_id: str, session: Session = Depends(get_session)) -> dict[str, Any]:
"""Get detailed transaction status""" """Get detailed transaction status"""
try: try:
@@ -416,7 +441,8 @@ async def get_transaction_status(transaction_id: str, session: Session = Depends
@router.post("/transactions/{transaction_id}/cancel", response_model=dict[str, Any]) @router.post("/transactions/{transaction_id}/cancel", response_model=dict[str, Any])
async def cancel_transaction(transaction_id: str, reason: str, session: Session = Depends(get_session)) -> dict[str, Any]: @rate_limit(rate=20, per=60)
async def cancel_transaction(request: Request, transaction_id: str, reason: str, session: Session = Depends(get_session)) -> dict[str, Any]:
"""Cancel a transaction""" """Cancel a transaction"""
try: try:
@@ -437,7 +463,9 @@ async def cancel_transaction(transaction_id: str, reason: str, session: Session
@router.get("/transactions/history", response_model=list[dict[str, Any]]) @router.get("/transactions/history", response_model=list[dict[str, Any]])
@rate_limit(rate=200, per=60)
async def get_transaction_history( async def get_transaction_history(
request: Request,
user_id: str | None = Query(None), user_id: str | None = Query(None),
chain_id: int | None = Query(None), chain_id: int | None = Query(None),
transaction_type: TransactionType | None = Query(None), transaction_type: TransactionType | None = Query(None),
@@ -479,7 +507,9 @@ async def get_transaction_history(
@router.get("/transactions/statistics", response_model=dict[str, Any]) @router.get("/transactions/statistics", response_model=dict[str, Any])
@rate_limit(rate=200, per=60)
async def get_transaction_statistics( async def get_transaction_statistics(
request: Request,
time_period_hours: int = Query(24, ge=1, le=8760), time_period_hours: int = Query(24, ge=1, le=8760),
chain_id: int | None = Query(None), chain_id: int | None = Query(None),
session: Session = Depends(get_session), session: Session = Depends(get_session),
@@ -504,7 +534,9 @@ async def get_transaction_statistics(
@router.post("/transactions/optimize-routing", response_model=dict[str, Any]) @router.post("/transactions/optimize-routing", response_model=dict[str, Any])
@rate_limit(rate=50, per=60)
async def optimize_transaction_routing( async def optimize_transaction_routing(
request: Request,
transaction_type: TransactionType, transaction_type: TransactionType,
amount: float, amount: float,
from_chain: int, from_chain: int,
@@ -535,7 +567,8 @@ async def optimize_transaction_routing(
# Configuration and Status Endpoints # Configuration and Status Endpoints
@router.get("/chains/supported", response_model=list[dict[str, Any]]) @router.get("/chains/supported", response_model=list[dict[str, Any]])
async def get_supported_chains() -> list[dict[str, Any]]: @rate_limit(rate=500, per=60)
async def get_supported_chains(request: Request) -> list[dict[str, Any]]:
"""Get list of supported blockchain chains""" """Get list of supported blockchain chains"""
try: try:
@@ -554,7 +587,8 @@ async def get_supported_chains() -> list[dict[str, Any]]:
@router.get("/chains/{chain_id}/info", response_model=dict[str, Any]) @router.get("/chains/{chain_id}/info", response_model=dict[str, Any])
async def get_chain_info(chain_id: int, session: Session = Depends(get_session)) -> dict[str, Any]: @rate_limit(rate=500, per=60)
async def get_chain_info(request: Request, chain_id: int, session: Session = Depends(get_session)) -> dict[str, Any]:
"""Get information about a specific chain""" """Get information about a specific chain"""
try: try:
@@ -576,7 +610,8 @@ async def get_chain_info(chain_id: int, session: Session = Depends(get_session))
@router.get("/health", response_model=dict[str, Any]) @router.get("/health", response_model=dict[str, Any])
async def get_cross_chain_health(session: Session = Depends(get_session)) -> dict[str, Any]: @rate_limit(rate=1000, per=60)
async def get_cross_chain_health(request: Request, session: Session = Depends(get_session)) -> dict[str, Any]:
"""Get cross-chain integration health status""" """Get cross-chain integration health status"""
try: try:
@@ -614,7 +649,8 @@ async def get_cross_chain_health(session: Session = Depends(get_session)) -> dic
@router.get("/config", response_model=dict[str, Any]) @router.get("/config", response_model=dict[str, Any])
async def get_cross_chain_config(session: Session = Depends(get_session)) -> dict[str, Any]: @rate_limit(rate=500, per=60)
async def get_cross_chain_config(request: Request, session: Session = Depends(get_session)) -> dict[str, Any]:
"""Get cross-chain integration configuration""" """Get cross-chain integration configuration"""
try: try:

View File

@@ -6,9 +6,11 @@ REST API endpoints for the developer ecosystem including bounties, certification
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query, Request
from sqlmodel import Session, func, select from sqlmodel import Session, func, select
from aitbc.rate_limiting import rate_limit
from ..domain.developer_platform import ( from ..domain.developer_platform import (
BountyStatus, BountyStatus,
CertificationLevel, CertificationLevel,
@@ -35,8 +37,9 @@ def get_governance_service(session: Session = Depends(get_session)) -> Governanc
# Developer Management Endpoints # Developer Management Endpoints
@router.post("/register", response_model=dict[str, Any]) @router.post("/register", response_model=dict[str, Any])
@rate_limit(rate=10, per=60)
async def register_developer( async def register_developer(
request: DeveloperCreate, request: DeveloperCreate, request_http: Request,
session: Session = Depends(get_session), session: Session = Depends(get_session),
dev_service: DeveloperPlatformService = Depends(get_developer_platform_service), dev_service: DeveloperPlatformService = Depends(get_developer_platform_service),
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -61,8 +64,9 @@ async def register_developer(
@router.get("/profile/{wallet_address}", response_model=dict[str, Any]) @router.get("/profile/{wallet_address}", response_model=dict[str, Any])
@rate_limit(rate=200, per=60)
async def get_developer_profile( async def get_developer_profile(
wallet_address: str, wallet_address: str, request: Request,
session: Session = Depends(get_session), session: Session = Depends(get_session),
dev_service: DeveloperPlatformService = Depends(get_developer_platform_service), dev_service: DeveloperPlatformService = Depends(get_developer_platform_service),
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -93,9 +97,11 @@ async def get_developer_profile(
@router.put("/profile/{wallet_address}", response_model=dict[str, Any]) @router.put("/profile/{wallet_address}", response_model=dict[str, Any])
@rate_limit(rate=50, per=60)
async def update_developer_profile( async def update_developer_profile(
wallet_address: str, wallet_address: str,
updates: dict[str, Any], updates: dict[str, Any],
request: Request,
session: Session = Depends(get_session), session: Session = Depends(get_session),
dev_service: DeveloperPlatformService = Depends(get_developer_platform_service), dev_service: DeveloperPlatformService = Depends(get_developer_platform_service),
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -119,9 +125,11 @@ async def update_developer_profile(
@router.get("/leaderboard", response_model=list[dict[str, Any]]) @router.get("/leaderboard", response_model=list[dict[str, Any]])
@rate_limit(rate=200, per=60)
async def get_leaderboard( async def get_leaderboard(
limit: int = Query(100, ge=1, le=500, description="Maximum number of developers"), limit: int = Query(100, ge=1, le=500, description="Maximum number of developers"),
offset: int = Query(0, ge=0, description="Offset for pagination"), offset: int = Query(0, ge=0, description="Offset for pagination"),
request: Request,
session: Session = Depends(get_session), session: Session = Depends(get_session),
dev_service: DeveloperPlatformService = Depends(get_developer_platform_service), dev_service: DeveloperPlatformService = Depends(get_developer_platform_service),
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
@@ -149,8 +157,9 @@ async def get_leaderboard(
@router.get("/stats/{wallet_address}", response_model=dict[str, Any]) @router.get("/stats/{wallet_address}", response_model=dict[str, Any])
@rate_limit(rate=200, per=60)
async def get_developer_stats( async def get_developer_stats(
wallet_address: str, wallet_address: str, request: Request,
session: Session = Depends(get_session), session: Session = Depends(get_session),
dev_service: DeveloperPlatformService = Depends(get_developer_platform_service), dev_service: DeveloperPlatformService = Depends(get_developer_platform_service),
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -168,8 +177,9 @@ async def get_developer_stats(
# Bounty Management Endpoints # Bounty Management Endpoints
@router.post("/bounties", response_model=dict[str, Any]) @router.post("/bounties", response_model=dict[str, Any])
@rate_limit(rate=20, per=60)
async def create_bounty( async def create_bounty(
request: BountyCreate, request: BountyCreate, request_http: Request,
session: Session = Depends(get_session), session: Session = Depends(get_session),
dev_service: DeveloperPlatformService = Depends(get_developer_platform_service), dev_service: DeveloperPlatformService = Depends(get_developer_platform_service),
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -195,10 +205,12 @@ async def create_bounty(
@router.get("/bounties", response_model=list[dict[str, Any]]) @router.get("/bounties", response_model=list[dict[str, Any]])
@rate_limit(rate=200, per=60)
async def list_bounties( async def list_bounties(
status: BountyStatus | None = Query(None, description="Filter by bounty status"), status: BountyStatus | None = Query(None, description="Filter by bounty status"),
limit: int = Query(100, ge=1, le=500, description="Maximum number of bounties"), limit: int = Query(100, ge=1, le=500, description="Maximum number of bounties"),
offset: int = Query(0, ge=0, description="Offset for pagination"), offset: int = Query(0, ge=0, description="Offset for pagination"),
request: Request,
session: Session = Depends(get_session), session: Session = Depends(get_session),
dev_service: DeveloperPlatformService = Depends(get_developer_platform_service), dev_service: DeveloperPlatformService = Depends(get_developer_platform_service),
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
@@ -228,8 +240,9 @@ async def list_bounties(
@router.get("/bounties/{bounty_id}", response_model=dict[str, Any]) @router.get("/bounties/{bounty_id}", response_model=dict[str, Any])
@rate_limit(rate=200, per=60)
async def get_bounty_details( async def get_bounty_details(
bounty_id: str, bounty_id: str, request: Request,
session: Session = Depends(get_session), session: Session = Depends(get_session),
dev_service: DeveloperPlatformService = Depends(get_developer_platform_service), dev_service: DeveloperPlatformService = Depends(get_developer_platform_service),
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -246,9 +259,11 @@ async def get_bounty_details(
@router.post("/bounties/{bounty_id}/submit", response_model=dict[str, Any]) @router.post("/bounties/{bounty_id}/submit", response_model=dict[str, Any])
@rate_limit(rate=20, per=60)
async def submit_bounty_solution( async def submit_bounty_solution(
bounty_id: str, bounty_id: str,
request: BountySubmissionCreate, request: BountySubmissionCreate,
request_http: Request,
session: Session = Depends(get_session), session: Session = Depends(get_session),
dev_service: DeveloperPlatformService = Depends(get_developer_platform_service), dev_service: DeveloperPlatformService = Depends(get_developer_platform_service),
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -275,10 +290,12 @@ async def submit_bounty_solution(
@router.get("/bounties/my-submissions", response_model=list[dict[str, Any]]) @router.get("/bounties/my-submissions", response_model=list[dict[str, Any]])
@rate_limit(rate=200, per=60)
async def get_my_submissions( async def get_my_submissions(
developer_id: str, developer_id: str,
limit: int = Query(100, ge=1, le=500, description="Maximum number of submissions"), limit: int = Query(100, ge=1, le=500, description="Maximum number of submissions"),
offset: int = Query(0, ge=0, description="Offset for pagination"), offset: int = Query(0, ge=0, description="Offset for pagination"),
request: Request,
session: Session = Depends(get_session), session: Session = Depends(get_session),
dev_service: DeveloperPlatformService = Depends(get_developer_platform_service), dev_service: DeveloperPlatformService = Depends(get_developer_platform_service),
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
@@ -308,11 +325,13 @@ async def get_my_submissions(
@router.post("/bounties/{bounty_id}/review", response_model=dict[str, Any]) @router.post("/bounties/{bounty_id}/review", response_model=dict[str, Any])
@rate_limit(rate=20, per=60)
async def review_bounty_submission( async def review_bounty_submission(
submission_id: str, submission_id: str,
reviewer_address: str, reviewer_address: str,
review_notes: str, review_notes: str,
approved: bool = Query(True, description="Whether to approve the submission"), approved: bool = Query(True, description="Whether to approve the submission"),
request: Request,
session: Session = Depends(get_session), session: Session = Depends(get_session),
dev_service: DeveloperPlatformService = Depends(get_developer_platform_service), dev_service: DeveloperPlatformService = Depends(get_developer_platform_service),
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -344,8 +363,9 @@ async def review_bounty_submission(
@router.get("/bounties/stats", response_model=dict[str, Any]) @router.get("/bounties/stats", response_model=dict[str, Any])
@rate_limit(rate=200, per=60)
async def get_bounty_statistics( async def get_bounty_statistics(
session: Session = Depends(get_session), dev_service: DeveloperPlatformService = Depends(get_developer_platform_service) request: Request, session: Session = Depends(get_session), dev_service: DeveloperPlatformService = Depends(get_developer_platform_service)
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Get comprehensive bounty statistics""" """Get comprehensive bounty statistics"""
@@ -359,8 +379,9 @@ async def get_bounty_statistics(
# Certification Management Endpoints # Certification Management Endpoints
@router.post("/certifications", response_model=dict[str, Any]) @router.post("/certifications", response_model=dict[str, Any])
@rate_limit(rate=20, per=60)
async def grant_certification( async def grant_certification(
request: CertificationGrant, request: CertificationGrant, request_http: Request,
session: Session = Depends(get_session), session: Session = Depends(get_session),
dev_service: DeveloperPlatformService = Depends(get_developer_platform_service), dev_service: DeveloperPlatformService = Depends(get_developer_platform_service),
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -388,8 +409,9 @@ async def grant_certification(
@router.get("/certifications/{wallet_address}", response_model=list[dict[str, Any]]) @router.get("/certifications/{wallet_address}", response_model=list[dict[str, Any]])
@rate_limit(rate=200, per=60)
async def get_developer_certifications( async def get_developer_certifications(
wallet_address: str, wallet_address: str, request: Request,
session: Session = Depends(get_session), session: Session = Depends(get_session),
dev_service: DeveloperPlatformService = Depends(get_developer_platform_service), dev_service: DeveloperPlatformService = Depends(get_developer_platform_service),
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:

View File

@@ -8,11 +8,12 @@ REST API for developer ecosystem metrics and analytics
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from aitbc import get_logger from aitbc import get_logger
from aitbc.rate_limiting import rate_limit
from ..auth import get_current_user from ..auth import get_current_user
from ..domain.bounty import AgentMetrics, BountyStats, EcosystemMetrics from ..domain.bounty import AgentMetrics, BountyStats, EcosystemMetrics
from ..services.ecosystem_service import EcosystemService from ..services.ecosystem_service import EcosystemService
@@ -88,7 +89,9 @@ def get_ecosystem_service(session: Session = Depends(get_session)) -> EcosystemS
# API endpoints # API endpoints
@router.get("/ecosystem/developer-earnings", response_model=DeveloperEarningsResponse) @router.get("/ecosystem/developer-earnings", response_model=DeveloperEarningsResponse)
@rate_limit(rate=200, per=60)
async def get_developer_earnings( async def get_developer_earnings(
request: Request,
period: str = Field(default="monthly", regex="^(daily|weekly|monthly)$"), period: str = Field(default="monthly", regex="^(daily|weekly|monthly)$"),
session: Session = Depends(get_session), session: Session = Depends(get_session),
ecosystem_service: EcosystemService = Depends(get_ecosystem_service), ecosystem_service: EcosystemService = Depends(get_ecosystem_service),
@@ -108,7 +111,9 @@ async def get_developer_earnings(
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
@router.get("/ecosystem/agent-utilization", response_model=AgentUtilizationResponse) @router.get("/ecosystem/agent-utilization", response_model=AgentUtilizationResponse)
@rate_limit(rate=200, per=60)
async def get_agent_utilization( async def get_agent_utilization(
request: Request,
period: str = Field(default="monthly", regex="^(daily|weekly|monthly)$"), period: str = Field(default="monthly", regex="^(daily|weekly|monthly)$"),
session: Session = Depends(get_session), session: Session = Depends(get_session),
ecosystem_service: EcosystemService = Depends(get_ecosystem_service) ecosystem_service: EcosystemService = Depends(get_ecosystem_service)
@@ -127,7 +132,9 @@ async def get_agent_utilization(
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
@router.get("/ecosystem/treasury-allocation", response_model=TreasuryAllocationResponse) @router.get("/ecosystem/treasury-allocation", response_model=TreasuryAllocationResponse)
@rate_limit(rate=200, per=60)
async def get_treasury_allocation( async def get_treasury_allocation(
request: Request,
period: str = Field(default="monthly", regex="^(daily|weekly|monthly)$"), period: str = Field(default="monthly", regex="^(daily|weekly|monthly)$"),
session: Session = Depends(get_session), session: Session = Depends(get_session),
ecosystem_service: EcosystemService = Depends(get_ecosystem_service) ecosystem_service: EcosystemService = Depends(get_ecosystem_service)
@@ -146,7 +153,9 @@ async def get_treasury_allocation(
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
@router.get("/ecosystem/staking-metrics", response_model=StakingMetricsResponse) @router.get("/ecosystem/staking-metrics", response_model=StakingMetricsResponse)
@rate_limit(rate=200, per=60)
async def get_staking_metrics( async def get_staking_metrics(
request: Request,
period: str = Field(default="monthly", regex="^(daily|weekly|monthly)$"), period: str = Field(default="monthly", regex="^(daily|weekly|monthly)$"),
session: Session = Depends(get_session), session: Session = Depends(get_session),
ecosystem_service: EcosystemService = Depends(get_ecosystem_service) ecosystem_service: EcosystemService = Depends(get_ecosystem_service)
@@ -165,7 +174,9 @@ async def get_staking_metrics(
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
@router.get("/ecosystem/bounty-analytics", response_model=BountyAnalyticsResponse) @router.get("/ecosystem/bounty-analytics", response_model=BountyAnalyticsResponse)
@rate_limit(rate=200, per=60)
async def get_bounty_analytics( async def get_bounty_analytics(
request: Request,
period: str = Field(default="monthly", regex="^(daily|weekly|monthly)$"), period: str = Field(default="monthly", regex="^(daily|weekly|monthly)$"),
session: Session = Depends(get_session), session: Session = Depends(get_session),
ecosystem_service: EcosystemService = Depends(get_ecosystem_service) ecosystem_service: EcosystemService = Depends(get_ecosystem_service)
@@ -184,7 +195,9 @@ async def get_bounty_analytics(
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
@router.get("/ecosystem/overview", response_model=EcosystemOverviewResponse) @router.get("/ecosystem/overview", response_model=EcosystemOverviewResponse)
@rate_limit(rate=100, per=60)
async def get_ecosystem_overview( async def get_ecosystem_overview(
request: Request,
period_type: str = Field(default="daily", regex="^(hourly|daily|weekly|monthly)$"), period_type: str = Field(default="daily", regex="^(hourly|daily|weekly|monthly)$"),
session: Session = Depends(get_session), session: Session = Depends(get_session),
ecosystem_service: EcosystemService = Depends(get_ecosystem_service) ecosystem_service: EcosystemService = Depends(get_ecosystem_service)
@@ -210,7 +223,9 @@ async def get_ecosystem_overview(
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
@router.get("/ecosystem/metrics") @router.get("/ecosystem/metrics")
@rate_limit(rate=200, per=60)
async def get_ecosystem_metrics( async def get_ecosystem_metrics(
request: Request,
period_type: str = Field(default="daily", regex="^(hourly|daily|weekly|monthly)$"), period_type: str = Field(default="daily", regex="^(hourly|daily|weekly|monthly)$"),
start_date: Optional[datetime] = None, start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None, end_date: Optional[datetime] = None,
@@ -238,9 +253,9 @@ async def get_ecosystem_metrics(
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
@router.get("/ecosystem/health-score") @router.get("/ecosystem/health-score")
@rate_limit(rate=200, per=60)
async def get_ecosystem_health_score( async def get_ecosystem_health_score(
session: Session = Depends(get_session), request: Request, session: Session = Depends(get_session), ecosystem_service: EcosystemService = Depends(get_ecosystem_service)
ecosystem_service: EcosystemService = Depends(get_ecosystem_service)
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Get overall ecosystem health score""" """Get overall ecosystem health score"""
try: try:
@@ -258,7 +273,9 @@ async def get_ecosystem_health_score(
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
@router.get("/ecosystem/growth-indicators") @router.get("/ecosystem/growth-indicators")
@rate_limit(rate=200, per=60)
async def get_growth_indicators( async def get_growth_indicators(
request: Request,
period: str = Field(default="monthly", regex="^(daily|weekly|monthly)$"), period: str = Field(default="monthly", regex="^(daily|weekly|monthly)$"),
session: Session = Depends(get_session), session: Session = Depends(get_session),
ecosystem_service: EcosystemService = Depends(get_ecosystem_service) ecosystem_service: EcosystemService = Depends(get_ecosystem_service)
@@ -279,7 +296,9 @@ async def get_growth_indicators(
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
@router.get("/ecosystem/top-performers") @router.get("/ecosystem/top-performers")
@rate_limit(rate=200, per=60)
async def get_top_performers( async def get_top_performers(
request: Request,
category: str = Field(default="all", regex="^(developers|agents|stakers|all)$"), category: str = Field(default="all", regex="^(developers|agents|stakers|all)$"),
period: str = Field(default="monthly", regex="^(daily|weekly|monthly)$"), period: str = Field(default="monthly", regex="^(daily|weekly|monthly)$"),
limit: int = Field(default=50, ge=1, le=100), limit: int = Field(default=50, ge=1, le=100),

View File

@@ -2,9 +2,11 @@ from __future__ import annotations
from typing import Annotated from typing import Annotated
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends, Query, Request
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from aitbc.rate_limiting import rate_limit
from ..schemas import ( from ..schemas import (
AddressListResponse, AddressListResponse,
BlockListResponse, BlockListResponse,
@@ -22,7 +24,9 @@ def _service(session: Annotated[Session, Depends(get_session)]) -> ExplorerServi
@router.get("/blocks", response_model=BlockListResponse, summary="List recent blocks") @router.get("/blocks", response_model=BlockListResponse, summary="List recent blocks")
@rate_limit(rate=100, per=60)
async def list_blocks( async def list_blocks(
request: Request,
*, *,
session: Annotated[Session, Depends(get_session)], session: Annotated[Session, Depends(get_session)],
limit: int = Query(default=20, ge=1, le=200), limit: int = Query(default=20, ge=1, le=200),
@@ -36,7 +40,9 @@ async def list_blocks(
response_model=TransactionListResponse, response_model=TransactionListResponse,
summary="List recent transactions", summary="List recent transactions",
) )
@rate_limit(rate=100, per=60)
async def list_transactions( async def list_transactions(
request: Request,
*, *,
session: Annotated[Session, Depends(get_session)], session: Annotated[Session, Depends(get_session)],
limit: int = Query(default=50, ge=1, le=200), limit: int = Query(default=50, ge=1, le=200),
@@ -46,7 +52,9 @@ async def list_transactions(
@router.get("/addresses", response_model=AddressListResponse, summary="List address summaries") @router.get("/addresses", response_model=AddressListResponse, summary="List address summaries")
@rate_limit(rate=100, per=60)
async def list_addresses( async def list_addresses(
request: Request,
*, *,
session: Annotated[Session, Depends(get_session)], session: Annotated[Session, Depends(get_session)],
limit: int = Query(default=50, ge=1, le=200), limit: int = Query(default=50, ge=1, le=200),
@@ -56,7 +64,9 @@ async def list_addresses(
@router.get("/receipts", response_model=ReceiptListResponse, summary="List job receipts") @router.get("/receipts", response_model=ReceiptListResponse, summary="List job receipts")
@rate_limit(rate=100, per=60)
async def list_receipts( async def list_receipts(
request: Request,
*, *,
session: Annotated[Session, Depends(get_session)], session: Annotated[Session, Depends(get_session)],
job_id: str | None = Query(default=None, description="Filter by job identifier"), job_id: str | None = Query(default=None, description="Filter by job identifier"),
@@ -67,7 +77,9 @@ async def list_receipts(
@router.get("/transactions/{tx_hash}", summary="Get transaction details by hash") @router.get("/transactions/{tx_hash}", summary="Get transaction details by hash")
@rate_limit(rate=100, per=60)
async def get_transaction( async def get_transaction(
request: Request,
*, *,
session: Annotated[Session, Depends(get_session)], session: Annotated[Session, Depends(get_session)],
tx_hash: str, tx_hash: str,

View File

@@ -11,16 +11,19 @@ from datetime import datetime, timezone
from typing import Any from typing import Any
import psutil import psutil
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends, Request
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from aitbc.rate_limiting import rate_limit
from ..storage import get_session from ..storage import get_session
router = APIRouter() router = APIRouter()
@router.get("/health", tags=["health"], summary="GPU Multi-Modal Service Health") @router.get("/health", tags=["health"], summary="GPU Multi-Modal Service Health")
async def gpu_multimodal_health(session: Annotated[Session, Depends(get_session)]) -> dict[str, Any]: @rate_limit(rate=1000, per=60)
async def gpu_multimodal_health(request: Request, session: Annotated[Session, Depends(get_session)]) -> dict[str, Any]:
""" """
Health check for GPU Multi-Modal Service (Port 8010) Health check for GPU Multi-Modal Service (Port 8010)
""" """
@@ -92,7 +95,8 @@ async def gpu_multimodal_health(session: Annotated[Session, Depends(get_session)
@router.get("/health/deep", tags=["health"], summary="Deep GPU Multi-Modal Service Health") @router.get("/health/deep", tags=["health"], summary="Deep GPU Multi-Modal Service Health")
async def gpu_multimodal_deep_health(session: Annotated[Session, Depends(get_session)]) -> dict[str, Any]: @rate_limit(rate=1000, per=60)
async def gpu_multimodal_deep_health(request: Request, session: Annotated[Session, Depends(get_session)]) -> dict[str, Any]:
""" """
Deep health check with CUDA performance validation Deep health check with CUDA performance validation
""" """

View File

@@ -10,9 +10,11 @@ from datetime import datetime, timezone
from typing import Any from typing import Any
import psutil import psutil
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends, Request
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from aitbc.rate_limiting import rate_limit
from aitbc import get_logger from aitbc import get_logger
from ..services.hermes_enhanced import hermesEnhancedService from ..services.hermes_enhanced import hermesEnhancedService
@@ -23,7 +25,8 @@ logger = get_logger(__name__)
@router.get("/health", tags=["health"], summary="hermes Enhanced Service Health") @router.get("/health", tags=["health"], summary="hermes Enhanced Service Health")
async def hermes_enhanced_health(session: Annotated[Session, Depends(get_session)]) -> dict[str, Any]: @rate_limit(rate=1000, per=60)
async def hermes_enhanced_health(request: Request, session: Annotated[Session, Depends(get_session)]) -> dict[str, Any]:
""" """
Health check for hermes Enhanced Service (Port 8007) Health check for hermes Enhanced Service (Port 8007)
""" """
@@ -101,7 +104,8 @@ async def hermes_enhanced_health(session: Annotated[Session, Depends(get_session
@router.get("/health/deep", tags=["health"], summary="Deep hermes Enhanced Service Health") @router.get("/health/deep", tags=["health"], summary="Deep hermes Enhanced Service Health")
async def hermes_enhanced_deep_health(session: Annotated[Session, Depends(get_session)]) -> dict[str, Any]: @rate_limit(rate=1000, per=60)
async def hermes_enhanced_deep_health(request: Request, session: Annotated[Session, Depends(get_session)]) -> dict[str, Any]:
""" """
Deep health check with hermes ecosystem validation Deep health check with hermes ecosystem validation
""" """

View File

@@ -10,9 +10,11 @@ from datetime import datetime, timezone
from typing import Any from typing import Any
import psutil import psutil
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends, Request
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from aitbc.rate_limiting import rate_limit
from aitbc import get_logger from aitbc import get_logger
from ..services.marketplace_enhanced import EnhancedMarketplaceService from ..services.marketplace_enhanced import EnhancedMarketplaceService
from ..storage import get_session from ..storage import get_session
@@ -24,7 +26,8 @@ router = APIRouter()
@router.get("/health", tags=["health"], summary="Enhanced Marketplace Service Health") @router.get("/health", tags=["health"], summary="Enhanced Marketplace Service Health")
async def marketplace_enhanced_health(session: Annotated[Session, Depends(get_session)]) -> dict[str, Any]: @rate_limit(rate=1000, per=60)
async def marketplace_enhanced_health(request: Request, session: Annotated[Session, Depends(get_session)]) -> dict[str, Any]:
""" """
Health check for Enhanced Marketplace Service (Port 8002) Health check for Enhanced Marketplace Service (Port 8002)
""" """
@@ -104,7 +107,8 @@ async def marketplace_enhanced_health(session: Annotated[Session, Depends(get_se
@router.get("/health/deep", tags=["health"], summary="Deep Enhanced Marketplace Service Health") @router.get("/health/deep", tags=["health"], summary="Deep Enhanced Marketplace Service Health")
async def marketplace_enhanced_deep_health(session: Annotated[Session, Depends(get_session)]) -> dict[str, Any]: @rate_limit(rate=1000, per=60)
async def marketplace_enhanced_deep_health(request: Request, session: Annotated[Session, Depends(get_session)]) -> dict[str, Any]:
""" """
Deep health check with marketplace feature validation Deep health check with marketplace feature validation
""" """

View File

@@ -8,10 +8,11 @@ import os
import sys import sys
from typing import Any from typing import Any
from fastapi import APIRouter, BackgroundTasks, HTTPException from fastapi import APIRouter, BackgroundTasks, HTTPException, Request
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from aitbc import get_logger from aitbc import get_logger
from aitbc.rate_limiting import rate_limit
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -88,7 +89,8 @@ class ScalingPolicyUpdate(BaseModel):
# Endpoints: GPU Optimization # Endpoints: GPU Optimization
@router.post("/gpu/allocate") @router.post("/gpu/allocate")
async def allocate_gpu_resources(request: GPUAllocationRequest) -> dict[str, Any]: @rate_limit(rate=50, per=60)
async def allocate_gpu_resources(request: Request, gpu_request: GPUAllocationRequest) -> dict[str, Any]:
"""Request optimal GPU resource allocation for a marketplace task""" """Request optimal GPU resource allocation for a marketplace task"""
try: try:
start_time = time.time() start_time = time.time()
@@ -108,7 +110,8 @@ async def allocate_gpu_resources(request: GPUAllocationRequest) -> dict[str, Any
@router.post("/gpu/release") @router.post("/gpu/release")
async def release_gpu_resources(request: GPUReleaseRequest) -> dict[str, str]: @rate_limit(rate=50, per=60)
async def release_gpu_resources(request: Request, gpu_request: GPUReleaseRequest) -> dict[str, str]:
"""Release previously allocated GPU resources""" """Release previously allocated GPU resources"""
success = gpu_optimizer.release_resources(request.job_id) success = gpu_optimizer.release_resources(request.job_id)
if not success: if not success:
@@ -117,14 +120,16 @@ async def release_gpu_resources(request: GPUReleaseRequest) -> dict[str, str]:
@router.get("/gpu/status") @router.get("/gpu/status")
async def get_gpu_status() -> dict[str, Any]: @rate_limit(rate=200, per=60)
async def get_gpu_status(request: Request) -> dict[str, Any]:
"""Get overall GPU fleet status and optimization metrics""" """Get overall GPU fleet status and optimization metrics"""
return gpu_optimizer.get_system_status() return gpu_optimizer.get_system_status()
# Endpoints: Distributed Processing # Endpoints: Distributed Processing
@router.post("/distributed/task") @router.post("/distributed/task")
async def submit_distributed_task(request: DistributedTaskRequest) -> dict[str, str]: @rate_limit(rate=50, per=60)
async def submit_distributed_task(request: Request, task_request: DistributedTaskRequest) -> dict[str, str]:
"""Submit a task to the distributed processing framework""" """Submit a task to the distributed processing framework"""
task = DistributedTask( task = DistributedTask(
task_id=None, task_id=None,
@@ -140,7 +145,8 @@ async def submit_distributed_task(request: DistributedTaskRequest) -> dict[str,
@router.get("/distributed/task/{task_id}") @router.get("/distributed/task/{task_id}")
async def get_distributed_task_status(task_id: str) -> dict[str, Any]: @rate_limit(rate=200, per=60)
async def get_distributed_task_status(request: Request, task_id: str) -> dict[str, Any]:
"""Check the status and get results of a distributed task""" """Check the status and get results of a distributed task"""
status = await distributed_coordinator.get_task_status(task_id) status = await distributed_coordinator.get_task_status(task_id)
if not status: if not status:
@@ -149,7 +155,8 @@ async def get_distributed_task_status(task_id: str) -> dict[str, Any]:
@router.post("/distributed/worker/register") @router.post("/distributed/worker/register")
async def register_worker(request: WorkerRegistrationRequest) -> dict[str, str]: @rate_limit(rate=20, per=60)
async def register_worker(request: Request, worker_request: WorkerRegistrationRequest) -> dict[str, str]:
"""Register a new worker node in the cluster""" """Register a new worker node in the cluster"""
distributed_coordinator.register_worker( distributed_coordinator.register_worker(
worker_id=request.worker_id, worker_id=request.worker_id,
@@ -161,14 +168,16 @@ async def register_worker(request: WorkerRegistrationRequest) -> dict[str, str]:
@router.get("/distributed/status") @router.get("/distributed/status")
async def get_cluster_status() -> dict[str, Any]: @rate_limit(rate=200, per=60)
async def get_cluster_status(request: Request) -> dict[str, Any]:
"""Get overall distributed cluster health and load""" """Get overall distributed cluster health and load"""
return distributed_coordinator.get_cluster_status() return distributed_coordinator.get_cluster_status()
# Endpoints: Caching # Endpoints: Caching
@router.get("/cache/stats") @router.get("/cache/stats")
async def get_cache_stats() -> dict[str, Any]: @rate_limit(rate=200, per=60)
async def get_cache_stats(request: Request) -> dict[str, Any]:
"""Get current caching performance statistics""" """Get current caching performance statistics"""
return { return {
"status": "connected" if cache_optimizer.is_connected else "local_only", "status": "connected" if cache_optimizer.is_connected else "local_only",
@@ -178,7 +187,8 @@ async def get_cache_stats() -> dict[str, Any]:
@router.post("/cache/invalidate/{namespace}") @router.post("/cache/invalidate/{namespace}")
async def invalidate_cache_namespace(namespace: str, background_tasks: BackgroundTasks) -> dict[str, str]: @rate_limit(rate=20, per=60)
async def invalidate_cache_namespace(request: Request, namespace: str, background_tasks: BackgroundTasks) -> dict[str, str]:
"""Invalidate a specific cache namespace (e.g., 'order_book')""" """Invalidate a specific cache namespace (e.g., 'order_book')"""
background_tasks.add_task(cache_optimizer.invalidate_namespace, namespace) background_tasks.add_task(cache_optimizer.invalidate_namespace, namespace)
return {"success": True, "message": f"Invalidation for {namespace} queued"} return {"success": True, "message": f"Invalidation for {namespace} queued"}
@@ -186,20 +196,23 @@ async def invalidate_cache_namespace(namespace: str, background_tasks: Backgroun
# Endpoints: Monitoring # Endpoints: Monitoring
@router.get("/monitor/dashboard") @router.get("/monitor/dashboard")
async def get_monitoring_dashboard() -> dict[str, Any]: @rate_limit(rate=200, per=60)
async def get_monitoring_dashboard(request: Request) -> dict[str, Any]:
"""Get real-time performance dashboard data""" """Get real-time performance dashboard data"""
return marketplace_monitor.get_realtime_dashboard_data() return marketplace_monitor.get_realtime_dashboard_data()
# Endpoints: Auto-scaling # Endpoints: Auto-scaling
@router.get("/scaler/status") @router.get("/scaler/status")
async def get_scaler_status() -> dict[str, Any]: @rate_limit(rate=200, per=60)
async def get_scaler_status(request: Request) -> dict[str, Any]:
"""Get current auto-scaler status and active rules""" """Get current auto-scaler status and active rules"""
return resource_scaler.get_status() return resource_scaler.get_status()
@router.post("/scaler/policy") @router.post("/scaler/policy")
async def update_scaling_policy(policy_update: ScalingPolicyUpdate) -> dict[str, str]: @rate_limit(rate=20, per=60)
async def update_scaling_policy(request: Request, policy_update: ScalingPolicyUpdate) -> dict[str, str]:
"""Update auto-scaling thresholds and parameters dynamically""" """Update auto-scaling thresholds and parameters dynamically"""
current_policy = resource_scaler.policy current_policy = resource_scaler.policy

View File

@@ -1,13 +1,16 @@
"""Monitor router for AITBC Coordinator API.""" """Monitor router for AITBC Coordinator API."""
from fastapi import APIRouter from fastapi import APIRouter, Request
from typing import List, Dict from typing import List, Dict
from aitbc.rate_limiting import rate_limit
router = APIRouter(tags=["Monitor"]) router = APIRouter(tags=["Monitor"])
@router.get("/api/v1/dashboard", response_model=dict) @router.get("/api/v1/dashboard", response_model=dict)
async def get_dashboard(): @rate_limit(rate=100, per=60)
async def get_dashboard(request: Request):
"""Get monitoring dashboard data.""" """Get monitoring dashboard data."""
return { return {
"overall_status": "operational", "overall_status": "operational",
@@ -26,7 +29,8 @@ async def get_dashboard():
@router.get("/status", response_model=dict) @router.get("/status", response_model=dict)
async def get_status(): @rate_limit(rate=100, per=60)
async def get_status(request: Request):
"""Get coordinator status.""" """Get coordinator status."""
return { return {
"status": "online", "status": "online",
@@ -37,18 +41,21 @@ async def get_status():
@router.get("/miners", response_model=List[Dict]) @router.get("/miners", response_model=List[Dict])
async def get_miners(): @rate_limit(rate=50, per=60)
async def get_miners(request: Request):
"""Get miners list.""" """Get miners list."""
return [] return []
@router.get("/dashboard", response_model=List[Dict]) @router.get("/dashboard", response_model=List[Dict])
async def get_history_dashboard(): @rate_limit(rate=50, per=60)
async def get_history_dashboard(request: Request):
"""Get historical dashboard data.""" """Get historical dashboard data."""
return [] return []
@router.get("/jobs", response_model=List[Dict]) @router.get("/jobs", response_model=List[Dict])
async def get_jobs(): @rate_limit(rate=50, per=60)
async def get_jobs(request: Request):
"""Get jobs list for history and metrics commands.""" """Get jobs list for history and metrics commands."""
return [] return []

View File

@@ -7,9 +7,11 @@ import asyncio
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any from typing import Any
from fastapi import APIRouter from fastapi import APIRouter, Depends, HTTPException, Query, Request
from sqlmodel import Session
from aitbc import get_logger, AITBCHTTPClient, NetworkError from aitbc import get_logger, AITBCHTTPClient, NetworkError
from aitbc.rate_limiting import rate_limit
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -63,7 +65,8 @@ SERVICES = {
@router.get("/dashboard", tags=["monitoring"], summary="Enhanced Services Dashboard") @router.get("/dashboard", tags=["monitoring"], summary="Enhanced Services Dashboard")
async def monitoring_dashboard() -> dict[str, Any]: @rate_limit(rate=200, per=60)
async def monitoring_dashboard(request: Request) -> dict[str, Any]:
""" """
Unified monitoring dashboard for all enhanced services Unified monitoring dashboard for all enhanced services
""" """

View File

@@ -10,9 +10,11 @@ from datetime import datetime, timezone
from typing import Any from typing import Any
import psutil import psutil
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends, Request
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from aitbc.rate_limiting import rate_limit
from ..services.multimodal_agent import MultiModalAgentService from ..services.multimodal_agent import MultiModalAgentService
from ..storage import get_session from ..storage import get_session
@@ -20,7 +22,8 @@ router = APIRouter()
@router.get("/health", tags=["health"], summary="Multi-Modal Agent Service Health") @router.get("/health", tags=["health"], summary="Multi-Modal Agent Service Health")
async def multimodal_health(session: Annotated[Session, Depends(get_session)]) -> dict[str, Any]: @rate_limit(rate=1000, per=60)
async def multimodal_health(request: Request, session: Annotated[Session, Depends(get_session)]) -> dict[str, Any]:
""" """
Health check for Multi-Modal Agent Service (Port 8002) Health check for Multi-Modal Agent Service (Port 8002)
""" """
@@ -87,7 +90,8 @@ async def multimodal_health(session: Annotated[Session, Depends(get_session)]) -
@router.get("/health/deep", tags=["health"], summary="Deep Multi-Modal Service Health") @router.get("/health/deep", tags=["health"], summary="Deep Multi-Modal Service Health")
async def multimodal_deep_health(session: Annotated[Session, Depends(get_session)]) -> dict[str, Any]: @rate_limit(rate=1000, per=60)
async def multimodal_deep_health(request: Request, session: Annotated[Session, Depends(get_session)]) -> dict[str, Any]:
""" """
Deep health check with detailed multi-modal processing tests Deep health check with detailed multi-modal processing tests
""" """

View File

@@ -11,9 +11,11 @@ import secrets
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any from typing import Any
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from aitbc.rate_limiting import rate_limit
from ..storage import get_session from ..storage import get_session
router = APIRouter(tags=["partners"]) router = APIRouter(tags=["partners"])
@@ -63,7 +65,8 @@ WEBHOOKS_DB = {}
@router.post("/partners/register", response_model=PartnerResponse) @router.post("/partners/register", response_model=PartnerResponse)
async def register_partner(partner: PartnerRegister, session: Annotated[Session, Depends(get_session)]) -> PartnerResponse: @rate_limit(rate=10, per=60)
async def register_partner(partner: PartnerRegister, request: Request, session: Annotated[Session, Depends(get_session)]) -> PartnerResponse:
"""Register a new partner application""" """Register a new partner application"""
# Generate credentials # Generate credentials
@@ -105,7 +108,8 @@ async def register_partner(partner: PartnerRegister, session: Annotated[Session,
@router.get("/partners/{partner_id}") @router.get("/partners/{partner_id}")
async def get_partner(partner_id: str, session: Annotated[Session, Depends(get_session)], api_key: str) -> dict[str, Any]: @rate_limit(rate=50, per=60)
async def get_partner(partner_id: str, request: Request, session: Annotated[Session, Depends(get_session)], api_key: str) -> dict[str, Any]:
"""Get partner information""" """Get partner information"""
# Verify API key # Verify API key
@@ -125,8 +129,9 @@ async def get_partner(partner_id: str, session: Annotated[Session, Depends(get_s
@router.post("/partners/webhooks", response_model=WebhookResponse) @router.post("/partners/webhooks", response_model=WebhookResponse)
@rate_limit(rate=20, per=60)
async def create_webhook( async def create_webhook(
webhook: WebhookCreate, session: Annotated[Session, Depends(get_session)], api_key: str webhook: WebhookCreate, request: Request, session: Annotated[Session, Depends(get_session)], api_key: str
) -> WebhookResponse: ) -> WebhookResponse:
"""Create a webhook subscription""" """Create a webhook subscription"""
@@ -175,7 +180,8 @@ async def create_webhook(
@router.get("/partners/webhooks") @router.get("/partners/webhooks")
async def list_webhooks(session: Annotated[Session, Depends(get_session)], api_key: str) -> list[WebhookResponse]: @rate_limit(rate=50, per=60)
async def list_webhooks(request: Request, session: Annotated[Session, Depends(get_session)], api_key: str) -> list[WebhookResponse]:
"""List partner webhooks""" """List partner webhooks"""
# Verify partner # Verify partner
@@ -201,7 +207,8 @@ async def list_webhooks(session: Annotated[Session, Depends(get_session)], api_k
@router.delete("/partners/webhooks/{webhook_id}") @router.delete("/partners/webhooks/{webhook_id}")
async def delete_webhook(webhook_id: str, session: Annotated[Session, Depends(get_session)], api_key: str) -> dict[str, str]: @rate_limit(rate=20, per=60)
async def delete_webhook(webhook_id: str, request: Request, session: Annotated[Session, Depends(get_session)], api_key: str) -> dict[str, str]:
"""Delete a webhook""" """Delete a webhook"""
# Verify partner # Verify partner
@@ -221,8 +228,9 @@ async def delete_webhook(webhook_id: str, session: Annotated[Session, Depends(ge
@router.get("/partners/analytics/usage") @router.get("/partners/analytics/usage")
@rate_limit(rate=30, per=60)
async def get_usage_analytics( async def get_usage_analytics(
session: Annotated[Session, Depends(get_session)], api_key: str, period: str = "24h" request: Request, session: Annotated[Session, Depends(get_session)], api_key: str, period: str = "24h"
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Get API usage analytics""" """Get API usage analytics"""

View File

@@ -12,9 +12,11 @@ import uuid
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any from typing import Any
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status, Request
from sqlmodel import Session, select from sqlmodel import Session, select
from aitbc.rate_limiting import rate_limit
from ..domain import User, Wallet from ..domain import User, Wallet
from ..schemas import UserBalance, UserCreate, UserLogin, UserProfile from ..schemas import UserBalance, UserCreate, UserLogin, UserProfile
from ..storage import get_session from ..storage import get_session
@@ -56,7 +58,8 @@ def verify_session_token(token: str) -> str | None:
@router.post("/register", response_model=UserProfile) @router.post("/register", response_model=UserProfile)
async def register_user(user_data: UserCreate, session: Annotated[Session, Depends(get_session)]) -> dict[str, Any]: @rate_limit(rate=10, per=60)
async def register_user(user_data: UserCreate, request: Request, session: Annotated[Session, Depends(get_session)]) -> dict[str, Any]:
"""Register a new user""" """Register a new user"""
# Check if user already exists # Check if user already exists
@@ -97,7 +100,8 @@ async def register_user(user_data: UserCreate, session: Annotated[Session, Depen
@router.post("/login", response_model=UserProfile) @router.post("/login", response_model=UserProfile)
async def login_user(login_data: UserLogin, session: Annotated[Session, Depends(get_session)]) -> dict[str, Any]: @rate_limit(rate=20, per=60)
async def login_user(login_data: UserLogin, request: Request, session: Annotated[Session, Depends(get_session)]) -> dict[str, Any]:
"""Login user with wallet address""" """Login user with wallet address"""
# For demo, we'll create or get user by wallet address # For demo, we'll create or get user by wallet address
@@ -144,7 +148,8 @@ async def login_user(login_data: UserLogin, session: Annotated[Session, Depends(
@router.get("/users/me", response_model=UserProfile) @router.get("/users/me", response_model=UserProfile)
async def get_current_user(token: str, session: Annotated[Session, Depends(get_session)]) -> dict[str, Any]: @rate_limit(rate=100, per=60)
async def get_current_user(token: str, request: Request, session: Annotated[Session, Depends(get_session)]) -> dict[str, Any]:
"""Get current user profile""" """Get current user profile"""
user_id = verify_session_token(token) user_id = verify_session_token(token)
@@ -165,7 +170,8 @@ async def get_current_user(token: str, session: Annotated[Session, Depends(get_s
@router.get("/users/{user_id}/balance", response_model=UserBalance) @router.get("/users/{user_id}/balance", response_model=UserBalance)
async def get_user_balance(user_id: str, session: Annotated[Session, Depends(get_session)]) -> dict[str, Any]: @rate_limit(rate=50, per=60)
async def get_user_balance(user_id: str, request: Request, session: Annotated[Session, Depends(get_session)]) -> dict[str, Any]:
"""Get user's AITBC balance""" """Get user's AITBC balance"""
wallet = session.execute(select(Wallet).where(Wallet.user_id == user_id)).first() wallet = session.execute(select(Wallet).where(Wallet.user_id == user_id)).first()
@@ -182,7 +188,8 @@ async def get_user_balance(user_id: str, session: Annotated[Session, Depends(get
@router.post("/logout") @router.post("/logout")
async def logout_user(token: str) -> dict[str, str]: @rate_limit(rate=20, per=60)
async def logout_user(token: str, request: Request) -> dict[str, str]:
"""Logout user and invalidate session""" """Logout user and invalidate session"""
if token in user_sessions: if token in user_sessions:
@@ -192,7 +199,8 @@ async def logout_user(token: str) -> dict[str, str]:
@router.get("/users/{user_id}/transactions") @router.get("/users/{user_id}/transactions")
async def get_user_transactions(user_id: str, session: Annotated[Session, Depends(get_session)]) -> dict[str, Any]: @rate_limit(rate=50, per=60)
async def get_user_transactions(user_id: str, request: Request, session: Annotated[Session, Depends(get_session)]) -> dict[str, Any]:
"""Get user's transaction history""" """Get user's transaction history"""
# For demo, return empty list # For demo, return empty list

View File

@@ -5,10 +5,12 @@ Web Vitals API endpoint for collecting performance metrics
from typing import Any from typing import Any
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from aitbc import get_logger from aitbc import get_logger
from aitbc.rate_limiting import rate_limit
from ..auth import get_api_key from ..auth import get_api_key
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -35,7 +37,8 @@ class WebVitalsMetric(BaseModel):
@router.post("/web-vitals") @router.post("/web-vitals")
async def collect_web_vitals(metric: WebVitalsMetric) -> dict[str, Any]: @rate_limit(rate=100, per=60)
async def collect_web_vitals(request: Request, metric: WebVitalsMetric) -> dict[str, Any]:
""" """
Collect Web Vitals performance metrics from the frontend. Collect Web Vitals performance metrics from the frontend.
This endpoint receives Core Web Vitals (LCP, FID, CLS, TTFB, FCP) for monitoring. This endpoint receives Core Web Vitals (LCP, FID, CLS, TTFB, FCP) for monitoring.
@@ -79,6 +82,7 @@ async def collect_web_vitals(metric: WebVitalsMetric) -> dict[str, Any]:
# Health check for web vitals endpoint # Health check for web vitals endpoint
@router.get("/web-vitals/health") @router.get("/web-vitals/health")
async def web_vitals_health() -> dict[str, str]: @rate_limit(rate=1000, per=60)
async def web_vitals_health(request: Request) -> dict[str, str]:
"""Health check for web vitals collection endpoint""" """Health check for web vitals collection endpoint"""
return {"status": "healthy", "service": "web-vitals"} return {"status": "healthy", "service": "web-vitals"}

View File

@@ -167,12 +167,45 @@
- Helper functions: is_feature_enabled(), get_feature_flag_manager() - Helper functions: is_feature_enabled(), get_feature_flag_manager()
- Comprehensive tests: tests/test_feature_flags.py (30+ test cases, 404 lines) - Comprehensive tests: tests/test_feature_flags.py (30+ test cases, 404 lines)
- Features: gradual rollouts, user whitelisting/blacklisting, percentage-based targeting, timestamp tracking - Features: gradual rollouts, user whitelisting/blacklisting, percentage-based targeting, timestamp tracking
- [ ] Implement comprehensive observability - [DONE] Implement comprehensive observability - COMPLETED
- [ ] Design contract upgrade pattern - aitbc/metrics.py implements Prometheus metrics (Counter, Histogram, Gauge, Info)
- Metrics for: block processing, job processing, API requests, uptime, service info
- Decorators: @track_block_processing, @track_job_processing, @track_http_request
- Helper functions: update_block_height, update_jobs_in_queue, increment_service_restarts
- ASGI metrics endpoint via make_asgi_app()
- aitbc/monitoring.py implements MetricsCollector, PerformanceTimer, HealthChecker
- Health checks with overall status calculation (healthy, degraded, unhealthy)
- Alerting exists in apps/agent-coordinator/src/app/monitoring/alerting.py and apps/coordinator-api/src/app/utils/alerting.py
- Comprehensive tests: tests/test_metrics.py (30+ test cases, 251 lines), tests/test_monitoring.py (30+ test cases, 353 lines)
- Enhanced aitbc/aitbc_logging.py with structured JSON logging (StructuredFormatter, log_context, LogContext)
- Created aitbc/tracing.py for OpenTelemetry-based distributed tracing
- Tracing features: setup_tracing, instrument_fastapi, instrument_httpx, instrument_sqlalchemy
- Decorators: trace_function, trace_async_function for automatic instrumentation
- Context manager: trace_span for manual span creation
- Created aitbc/alerting.py for centralized alerting system (AlertManager, AlertRule, AlertChannel)
- Created metrics dashboard configuration at infra/monitoring/aitbc-dashboard.json
- All observability components tested and imports verified
- [DONE] Design contract upgrade pattern - COMPLETED
- apps/blockchain-node/src/aitbc_chain/contracts/upgrades.py implements comprehensive contract upgrade system (543 lines)
- Core components: UpgradeStatus enum, UpgradeType enum, ContractVersion dataclass, UpgradeProposal dataclass
- ContractUpgradeManager with proposal creation, stake-weighted governance voting, upgrade execution, rollback mechanism
- Features: voting deadlines (3-7 days), 60% approval requirement, 30% minimum participation, emergency upgrades (80% threshold)
- Rollback window (7 days), version history tracking, upgrade statistics
- Contract examples: guardian_contract.py (683 lines), agent_messaging_contract.py (520 lines)
- Global upgrade manager singleton pattern
- Security: proposer authorization, version validation, proposal deduplication
### Distribution & Binaries ### Distribution & Binaries
- [ ] Debian stable miner binary (build workflow exists, binary built but distribution mechanism pending) - [DONE] Debian stable miner binary - COMPLETED
- Build workflow exists: .gitea/workflows/build-miner-binary.yml
- Binary built using PyInstaller with vLLM and Ollama support
- Package includes: binary, README.md, install.sh, verify-install.sh, miner.env.template, SHA256SUMS
- Distribution mechanism implemented: Gitea releases API integration
- Updated build workflow to create Gitea releases and upload assets automatically
- Updated README.md to reference Gitea releases instead of GitHub
- Binary and package uploaded to Gitea releases on tag push
- Checksum verification supported via SHA256SUMS file
- [ ] Binary distribution via GitHub Releases (deferred until v1 release - policy: no GitHub Releases before v1) - [ ] Binary distribution via GitHub Releases (deferred until v1 release - policy: no GitHub Releases before v1)
### Quality Assurance ### Quality Assurance

View File

@@ -0,0 +1,260 @@
{
"dashboard": {
"title": "AITBC System Dashboard",
"tags": ["aitbc", "blockchain", "ai"],
"timezone": "browser",
"schemaVersion": 16,
"version": 0,
"refresh": "10s",
"panels": [
{
"id": 1,
"title": "Block Processing Duration",
"type": "graph",
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 0},
"targets": [
{
"expr": "histogram_quantile(0.95, rate(block_processing_duration_seconds_bucket[5m]))",
"legendFormat": "95th percentile",
"refId": "A"
},
{
"expr": "histogram_quantile(0.50, rate(block_processing_duration_seconds_bucket[5m]))",
"legendFormat": "50th percentile",
"refId": "B"
}
],
"yaxes": [
{
"format": "s",
"label": "Duration"
},
{
"format": "short"
}
],
"xaxis": {
"mode": "time",
"name": "Time",
"show": true
}
},
{
"id": 2,
"title": "Block Height",
"type": "graph",
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 0},
"targets": [
{
"expr": "block_height",
"legendFormat": "Block Height",
"refId": "A"
}
],
"yaxes": [
{
"format": "short",
"label": "Block Number"
},
{
"format": "short"
}
]
},
{
"id": 3,
"title": "Job Processing Duration",
"type": "graph",
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 8},
"targets": [
{
"expr": "histogram_quantile(0.95, rate(job_processing_duration_seconds_bucket[5m]))",
"legendFormat": "95th percentile",
"refId": "A"
},
{
"expr": "histogram_quantile(0.50, rate(job_processing_duration_seconds_bucket[5m]))",
"legendFormat": "50th percentile",
"refId": "B"
}
],
"yaxes": [
{
"format": "s",
"label": "Duration"
},
{
"format": "short"
}
]
},
{
"id": 4,
"title": "Jobs in Queue",
"type": "graph",
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 8},
"targets": [
{
"expr": "jobs_in_queue",
"legendFormat": "Queued Jobs",
"refId": "A"
}
],
"yaxes": [
{
"format": "short",
"label": "Job Count"
},
{
"format": "short"
}
]
},
{
"id": 5,
"title": "HTTP Request Duration",
"type": "graph",
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 16},
"targets": [
{
"expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))",
"legendFormat": "95th percentile",
"refId": "A"
},
{
"expr": "histogram_quantile(0.50, rate(http_request_duration_seconds_bucket[5m]))",
"legendFormat": "50th percentile",
"refId": "B"
}
],
"yaxes": [
{
"format": "s",
"label": "Duration"
},
{
"format": "short"
}
]
},
{
"id": 6,
"title": "HTTP Requests by Status",
"type": "graph",
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 16},
"targets": [
{
"expr": "rate(http_requests_total[5m])",
"legendFormat": "{{method}} {{endpoint}} {{status}}",
"refId": "A"
}
],
"yaxes": [
{
"format": "reqps",
"label": "Requests/sec"
},
{
"format": "short"
}
]
},
{
"id": 7,
"title": "Job Success Rate",
"type": "stat",
"gridPos": {"h": 4, "w": 6, "x": 0, "y": 24},
"targets": [
{
"expr": "rate(jobs_total{status=\"completed\"}[5m]) / rate(jobs_total[5m]) * 100",
"legendFormat": "Success Rate",
"refId": "A"
}
],
"options": {
"colorMode": "value",
"graphMode": "area",
"orientation": "auto"
},
"fieldConfig": {
"defaults": {
"unit": "percent",
"min": 0,
"max": 100
}
}
},
{
"id": 8,
"title": "Job Failure Rate",
"type": "stat",
"gridPos": {"h": 4, "w": 6, "x": 6, "y": 24},
"targets": [
{
"expr": "rate(jobs_total{status=\"failed\"}[5m]) / rate(jobs_total[5m]) * 100",
"legendFormat": "Failure Rate",
"refId": "A"
}
],
"options": {
"colorMode": "value",
"graphMode": "area",
"orientation": "auto"
},
"fieldConfig": {
"defaults": {
"unit": "percent",
"min": 0,
"max": 100
}
}
},
{
"id": 9,
"title": "Service Uptime",
"type": "stat",
"gridPos": {"h": 4, "w": 6, "x": 12, "y": 24},
"targets": [
{
"expr": "service_uptime_seconds",
"legendFormat": "Uptime",
"refId": "A"
}
],
"options": {
"colorMode": "value",
"graphMode": "area",
"orientation": "auto"
},
"fieldConfig": {
"defaults": {
"unit": "s"
}
}
},
{
"id": 10,
"title": "Service Restarts",
"type": "stat",
"gridPos": {"h": 4, "w": 6, "x": 18, "y": 24},
"targets": [
{
"expr": "service_restart_count",
"legendFormat": "Restarts",
"refId": "A"
}
],
"options": {
"colorMode": "value",
"graphMode": "area",
"orientation": "auto"
},
"fieldConfig": {
"defaults": {
"unit": "short"
}
}
}
]
}
}