UPDATE
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
from .celery import app as celery_app
|
||||
|
||||
__all__ = ("celery_app",)
|
||||
@@ -0,0 +1,7 @@
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
||||
|
||||
application = get_asgi_application()
|
||||
@@ -0,0 +1,9 @@
|
||||
import os
|
||||
|
||||
from celery import Celery
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
||||
|
||||
app = Celery("config")
|
||||
app.config_from_object("django.conf:settings", namespace="CELERY")
|
||||
app.autodiscover_tasks()
|
||||
@@ -0,0 +1,53 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class FailureContract:
|
||||
status: str = "error"
|
||||
error_code: str = "internal_error"
|
||||
message: str = ""
|
||||
source: str = "application"
|
||||
warnings: list[str] = field(default_factory=list)
|
||||
retriable: bool = False
|
||||
details: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
payload = {
|
||||
"status": self.status,
|
||||
"error_code": self.error_code,
|
||||
"message": self.message,
|
||||
"source": self.source,
|
||||
"warnings": list(self.warnings),
|
||||
"retriable": self.retriable,
|
||||
}
|
||||
if self.details:
|
||||
payload["details"] = self.details
|
||||
return payload
|
||||
|
||||
|
||||
class StructuredServiceError(Exception):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
error_code: str,
|
||||
message: str,
|
||||
source: str,
|
||||
warnings: list[str] | None = None,
|
||||
retriable: bool = False,
|
||||
details: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
super().__init__(message)
|
||||
self.contract = FailureContract(
|
||||
error_code=error_code,
|
||||
message=message,
|
||||
source=source,
|
||||
warnings=warnings or [],
|
||||
retriable=retriable,
|
||||
details=details or {},
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return self.contract.to_dict()
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"auth": "auth_access",
|
||||
"account": "account_management",
|
||||
"farm_hub": "farm_management",
|
||||
"access_control": "access_control",
|
||||
"sensor_catalog": "sensor_catalog",
|
||||
"dashboard": "farm_dashboard",
|
||||
"crop_zoning": "crop_zoning",
|
||||
"plant_simulator": "plant_simulator",
|
||||
"pest_detection": "pest_detection",
|
||||
"sensor_7_in_1": "sensor-7-in-1",
|
||||
"irrigation": "irrigation",
|
||||
"fertilization": "fertilization",
|
||||
"farm_ai_assistant": "farm_ai_assistant",
|
||||
"notifications": "notifications",
|
||||
"external_api_adapter": "external_api_adapter",
|
||||
"sensor_external_api": "sensor_external_api"
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _isoformat(value: Any) -> Any:
|
||||
if isinstance(value, datetime):
|
||||
return value.isoformat()
|
||||
return value
|
||||
|
||||
|
||||
def build_integration_meta(
|
||||
*,
|
||||
flow_type: str,
|
||||
source_type: str,
|
||||
source_service: str,
|
||||
ownership: str,
|
||||
live: bool,
|
||||
cached: bool,
|
||||
generated_at: Any = None,
|
||||
snapshot_at: Any = None,
|
||||
sync_attempted: bool | None = None,
|
||||
sync_status: str | None = None,
|
||||
notes: list[str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
meta = {
|
||||
"flow_type": flow_type,
|
||||
"source_type": source_type,
|
||||
"source_service": source_service,
|
||||
"ownership": ownership,
|
||||
"live": live,
|
||||
"cached": cached,
|
||||
}
|
||||
if generated_at is not None:
|
||||
meta["generated_at"] = _isoformat(generated_at)
|
||||
if snapshot_at is not None:
|
||||
meta["snapshot_at"] = _isoformat(snapshot_at)
|
||||
if sync_attempted is not None:
|
||||
meta["sync_attempted"] = sync_attempted
|
||||
if sync_status is not None:
|
||||
meta["sync_status"] = sync_status
|
||||
if notes:
|
||||
meta["notes"] = notes
|
||||
return meta
|
||||
@@ -0,0 +1,129 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
from collections import Counter
|
||||
from contextvars import ContextVar
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_request_id_ctx: ContextVar[str | None] = ContextVar("backend_request_id", default=None)
|
||||
METRICS: Counter[str] = Counter()
|
||||
|
||||
|
||||
def set_request_id(request_id: str | None) -> None:
|
||||
_request_id_ctx.set(request_id)
|
||||
|
||||
|
||||
def get_request_id() -> str | None:
|
||||
return _request_id_ctx.get()
|
||||
|
||||
|
||||
def record_metric(name: str, value: int = 1, **tags: Any) -> None:
|
||||
suffix = ",".join(f"{key}={tags[key]}" for key in sorted(tags) if tags[key] is not None)
|
||||
metric_key = f"{name}|{suffix}" if suffix else name
|
||||
METRICS[metric_key] += value
|
||||
|
||||
|
||||
@dataclass
|
||||
class ClassifiedFailure:
|
||||
error_code: str
|
||||
failure_type: str
|
||||
retriable: bool
|
||||
|
||||
|
||||
def classify_exception(exc: Exception) -> ClassifiedFailure:
|
||||
exc_name = exc.__class__.__name__.lower()
|
||||
message = str(exc).lower()
|
||||
if "timeout" in exc_name or "timeout" in message:
|
||||
return ClassifiedFailure("timeout", "timeout", True)
|
||||
if "json" in exc_name or "json" in message:
|
||||
return ClassifiedFailure("parse_error", "parse_error", False)
|
||||
if "validation" in exc_name or "invalid" in message:
|
||||
return ClassifiedFailure("validation_failure", "validation_failure", False)
|
||||
if "connection" in exc_name or "unavailable" in message:
|
||||
return ClassifiedFailure("dependency_unavailable", "dependency_unavailable", True)
|
||||
return ClassifiedFailure("provider_error", "provider_error", True)
|
||||
|
||||
|
||||
def log_event(
|
||||
*,
|
||||
level: int,
|
||||
message: str,
|
||||
source: str,
|
||||
provider: str | None,
|
||||
operation: str,
|
||||
result_status: str,
|
||||
duration_ms: float | None = None,
|
||||
error_code: str | None = None,
|
||||
**extra: Any,
|
||||
) -> None:
|
||||
payload = {
|
||||
"source": source,
|
||||
"provider": provider,
|
||||
"operation": operation,
|
||||
"result_status": result_status,
|
||||
"duration_ms": round(duration_ms, 2) if duration_ms is not None else None,
|
||||
"error_code": error_code,
|
||||
"request_id": get_request_id(),
|
||||
}
|
||||
payload.update({key: value for key, value in extra.items() if value is not None})
|
||||
logger.log(level, message, extra={"event": payload})
|
||||
|
||||
|
||||
class observe_operation:
|
||||
def __init__(self, *, source: str, provider: str | None, operation: str):
|
||||
self.source = source
|
||||
self.provider = provider
|
||||
self.operation = operation
|
||||
self.started_at = 0.0
|
||||
|
||||
def __enter__(self):
|
||||
self.started_at = time.monotonic()
|
||||
log_event(
|
||||
level=logging.INFO,
|
||||
message="backend operation started",
|
||||
source=self.source,
|
||||
provider=self.provider,
|
||||
operation=self.operation,
|
||||
result_status="started",
|
||||
)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, _tb):
|
||||
duration_ms = (time.monotonic() - self.started_at) * 1000
|
||||
if exc is None:
|
||||
log_event(
|
||||
level=logging.INFO,
|
||||
message="backend operation completed",
|
||||
source=self.source,
|
||||
provider=self.provider,
|
||||
operation=self.operation,
|
||||
result_status="success",
|
||||
duration_ms=duration_ms,
|
||||
)
|
||||
record_metric("backend.operation.success", source=self.source, provider=self.provider, operation=self.operation)
|
||||
return False
|
||||
|
||||
failure = classify_exception(exc)
|
||||
log_event(
|
||||
level=logging.ERROR,
|
||||
message="backend operation failed",
|
||||
source=self.source,
|
||||
provider=self.provider,
|
||||
operation=self.operation,
|
||||
result_status="error",
|
||||
duration_ms=duration_ms,
|
||||
error_code=failure.error_code,
|
||||
failure_type=failure.failure_type,
|
||||
)
|
||||
record_metric(
|
||||
"backend.operation.failure",
|
||||
source=self.source,
|
||||
provider=self.provider,
|
||||
operation=self.operation,
|
||||
error_code=failure.error_code,
|
||||
)
|
||||
return False
|
||||
@@ -0,0 +1,294 @@
|
||||
import os
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from celery.schedules import crontab
|
||||
|
||||
load_dotenv()
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
LOG_DIR = BASE_DIR / "logs"
|
||||
LOG_DIR.mkdir(exist_ok=True)
|
||||
|
||||
|
||||
def _get_csv_env(name, default=""):
|
||||
return [item.strip() for item in os.environ.get(name, default).split(",") if item.strip()]
|
||||
|
||||
SECRET_KEY = os.environ.get("SECRET_KEY", "django-insecure-dev-only")
|
||||
DEBUG = os.environ.get("DEBUG", "0") == "1"
|
||||
ALLOWED_HOSTS = list(
|
||||
dict.fromkeys(
|
||||
_get_csv_env("ALLOWED_HOSTS", "localhost,127.0.0.1,0.0.0.0")
|
||||
+ ["web", "backend-web", os.environ.get("HOSTNAME", "")]
|
||||
)
|
||||
)
|
||||
|
||||
AUTH_USER_MODEL = "account.User"
|
||||
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
"account.backends.MultiFieldBackend",
|
||||
]
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"auth.apps.AuthConfig",
|
||||
"account.apps.AccountConfig",
|
||||
"farm_hub.apps.FarmHubConfig",
|
||||
"device_hub.apps.DeviceHubConfig",
|
||||
"access_control.apps.AccessControlConfig",
|
||||
"dashboard",
|
||||
"crop_health.apps.CropHealthConfig",
|
||||
"soil.apps.SoilConfig",
|
||||
"crop_zoning",
|
||||
"pest_detection",
|
||||
"water.apps.WaterConfig",
|
||||
"irrigation",
|
||||
"yield_harvest.apps.YieldHarvestConfig",
|
||||
"economic_overview.apps.EconomicOverviewConfig",
|
||||
"farm_alerts.apps.FarmAlertsConfig",
|
||||
"fertilization",
|
||||
"farm_ai_assistant",
|
||||
"notifications.apps.NotificationsConfig",
|
||||
"plants.apps.PlantsConfig",
|
||||
"farmer_calendar.apps.FarmerCalendarConfig",
|
||||
"farmer_todos.apps.FarmerTodosConfig",
|
||||
"external_api_adapter.apps.ExternalApiAdapterConfig",
|
||||
"rest_framework",
|
||||
"drf_spectacular",
|
||||
"drf_spectacular_sidecar",
|
||||
"corsheaders",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"corsheaders.middleware.CorsMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"access_control.middleware.RouteFeatureAccessMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "config.urls"
|
||||
WSGI_APPLICATION = "config.wsgi.application"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": os.environ.get("DB_ENGINE", "django.db.backends.mysql"),
|
||||
"NAME": os.environ.get("DB_NAME", "croplogic"),
|
||||
"USER": os.environ.get("DB_USER", "croplogic"),
|
||||
"PASSWORD": os.environ.get("DB_PASSWORD", ""),
|
||||
"HOST": os.environ.get("DB_HOST", "127.0.0.1"),
|
||||
"PORT": os.environ.get("DB_PORT", "3306"),
|
||||
"OPTIONS": {
|
||||
"charset": "utf8mb4",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
|
||||
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
|
||||
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
|
||||
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
|
||||
]
|
||||
|
||||
LANGUAGE_CODE = "en-us"
|
||||
TIME_ZONE = "UTC"
|
||||
USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
STATIC_URL = "static/"
|
||||
STATIC_ROOT = BASE_DIR / "staticfiles"
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django.core.cache.backends.redis.RedisCache",
|
||||
"LOCATION": os.getenv("CACHE_URL", os.getenv("CELERY_BROKER_URL", "redis://redis:6379/0")),
|
||||
"KEY_PREFIX": "croplogic",
|
||||
}
|
||||
}
|
||||
|
||||
PEST_DISEASE_RISK_SUMMARY_CACHE_TTL = int(os.getenv("PEST_DISEASE_RISK_SUMMARY_CACHE_TTL", "14400"))
|
||||
WATER_NEED_PREDICTION_CACHE_TTL = int(os.getenv("WATER_NEED_PREDICTION_CACHE_TTL", "14400"))
|
||||
SOIL_SUMMARY_CACHE_TTL = int(os.getenv("SOIL_SUMMARY_CACHE_TTL", "14400"))
|
||||
SOIL_ANOMALIES_CACHE_TTL = int(os.getenv("SOIL_ANOMALIES_CACHE_TTL", "14400"))
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
"DEFAULT_PERMISSION_CLASSES": [
|
||||
"rest_framework.permissions.IsAuthenticated",
|
||||
"access_control.permissions.FeatureAccessPermission",
|
||||
],
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": [
|
||||
"rest_framework_simplejwt.authentication.JWTAuthentication",
|
||||
],
|
||||
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
||||
}
|
||||
|
||||
SPECTACULAR_SETTINGS = {
|
||||
"TITLE": "CropLogic API",
|
||||
"DESCRIPTION": "Swagger/OpenAPI documentation for all CropLogic API endpoints.",
|
||||
"VERSION": "1.0.0",
|
||||
"SERVE_INCLUDE_SCHEMA": False,
|
||||
"SWAGGER_UI_DIST": "SIDECAR",
|
||||
"SWAGGER_UI_FAVICON_HREF": "SIDECAR",
|
||||
"REDOC_DIST": "SIDECAR",
|
||||
"SCHEMA_PATH_PREFIX": r"/api/",
|
||||
"SERVE_PERMISSIONS": ["rest_framework.permissions.AllowAny"],
|
||||
"APPEND_COMPONENTS": {
|
||||
"securitySchemes": {
|
||||
"SensorExternalApiKey": {
|
||||
"type": "apiKey",
|
||||
"in": "header",
|
||||
"name": "X-API-Key",
|
||||
"description": "Use API key 12345 for sensor external API endpoints.",
|
||||
}
|
||||
}
|
||||
},
|
||||
"SWAGGER_UI_SETTINGS": {
|
||||
"persistAuthorization": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
SMS_IR_API_KEY = os.environ.get("SMS_IR_API_KEY", "")
|
||||
SMS_IR_LINE_NUMBER = int(os.environ.get("SMS_IR_LINE_NUMBER", "300000000000"))
|
||||
|
||||
CORS_ALLOW_ALL_ORIGINS = DEBUG
|
||||
|
||||
USE_EXTERNAL_API_MOCK = os.getenv("USE_EXTERNAL_API_MOCK", "false").lower() == "true"
|
||||
EXTERNAL_API_TIMEOUT = int(os.getenv("EXTERNAL_API_TIMEOUT", "30"))
|
||||
|
||||
ACCESS_CONTROL_AUTHZ_ENABLED = os.getenv("ACCESS_CONTROL_AUTHZ_ENABLED", "true").lower() == "true"
|
||||
ACCESS_CONTROL_AUTHZ_BASE_URL = os.getenv(
|
||||
"ACCESS_CONTROL_AUTHZ_BASE_URL",
|
||||
"http://croplogic-accsess-opa:8181",
|
||||
)
|
||||
ACCESS_CONTROL_AUTHZ_BATCH_PATH = os.getenv("ACCESS_CONTROL_AUTHZ_BATCH_PATH", "/v1/data/croplogic/authz/batch_decision")
|
||||
ACCESS_CONTROL_AUTHZ_TIMEOUT = int(os.getenv("ACCESS_CONTROL_AUTHZ_TIMEOUT", str(EXTERNAL_API_TIMEOUT)))
|
||||
ACCESS_CONTROL_AUTHZ_CACHE_TIMEOUT = int(os.getenv("ACCESS_CONTROL_AUTHZ_CACHE_TIMEOUT", "300"))
|
||||
|
||||
EXTERNAL_SERVICES = {
|
||||
"ai": {
|
||||
"base_url": os.getenv("AI_SERVICE_BASE_URL", "http://ai-web:8000"),
|
||||
"api_key": os.getenv("AI_SERVICE_API_KEY", ""),
|
||||
"host_header": os.getenv("AI_SERVICE_HOST_HEADER", "localhost"),
|
||||
},
|
||||
"farm_hub": {
|
||||
"base_url": os.getenv("FARM_HUB_SERVICE_BASE_URL", ""),
|
||||
"api_key": os.getenv("FARM_HUB_SERVICE_API_KEY", ""),
|
||||
"host_header": os.getenv("FARM_HUB_SERVICE_HOST_HEADER", ""),
|
||||
},
|
||||
}
|
||||
|
||||
SIMPLE_JWT = {
|
||||
"ACCESS_TOKEN_LIFETIME": timedelta(days=7),
|
||||
"REFRESH_TOKEN_LIFETIME": timedelta(days=7),
|
||||
"ROTATE_REFRESH_TOKENS": False,
|
||||
"BLACKLIST_AFTER_ROTATION": False,
|
||||
}
|
||||
|
||||
CROP_ZONE_CHUNK_AREA_SQM = float(os.getenv("CROP_ZONE_CHUNK_AREA_SQM", "10000"))
|
||||
CROP_ZONE_TASK_STALE_SECONDS = int(os.getenv("CROP_ZONE_TASK_STALE_SECONDS", "300"))
|
||||
|
||||
CELERY_BROKER_URL = os.getenv("CELERY_BROKER_URL", "redis://redis:6379/0")
|
||||
CELERY_RESULT_BACKEND = os.getenv("CELERY_RESULT_BACKEND", CELERY_BROKER_URL)
|
||||
NOTIFICATION_REDIS_URL = os.getenv("NOTIFICATION_REDIS_URL", CELERY_BROKER_URL)
|
||||
EXTERNAL_NOTIFICATION_API_KEY = os.getenv("EXTERNAL_NOTIFICATION_API_KEY", "12345")
|
||||
SENSOR_EXTERNAL_API_KEY = os.getenv("SENSOR_EXTERNAL_API_KEY", "12345")
|
||||
FARM_DATA_API_HOST = os.getenv("FARM_DATA_API_HOST", "")
|
||||
FARM_DATA_API_PORT = os.getenv("FARM_DATA_API_PORT", "")
|
||||
FARM_DATA_API_PATH = os.getenv("FARM_DATA_API_PATH", "/api/farm-data/")
|
||||
FARM_DATA_API_KEY = os.getenv("FARM_DATA_API_KEY", "")
|
||||
FARM_DATA_API_TIMEOUT = int(os.getenv("FARM_DATA_API_TIMEOUT", str(EXTERNAL_API_TIMEOUT)))
|
||||
CELERY_TASK_DEFAULT_QUEUE = os.getenv("CELERY_TASK_DEFAULT_QUEUE", "default")
|
||||
CELERY_TASK_ACKS_LATE = True
|
||||
CELERY_WORKER_PREFETCH_MULTIPLIER = int(os.getenv("CELERY_WORKER_PREFETCH_MULTIPLIER", "1"))
|
||||
CELERY_TASK_TIME_LIMIT = int(os.getenv("CELERY_TASK_TIME_LIMIT", "120"))
|
||||
CELERY_TASK_SOFT_TIME_LIMIT = int(os.getenv("CELERY_TASK_SOFT_TIME_LIMIT", "90"))
|
||||
CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = os.getenv("CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP", "true").lower() == "true"
|
||||
FARM_ALERTS_AI_SYNC_CRON_MINUTE = os.getenv("FARM_ALERTS_AI_SYNC_CRON_MINUTE", "0")
|
||||
FARM_ALERTS_AI_SYNC_CRON_HOUR = os.getenv("FARM_ALERTS_AI_SYNC_CRON_HOUR", "*")
|
||||
|
||||
CELERY_BEAT_SCHEDULE = {
|
||||
"sync-farm-alert-trackers": {
|
||||
"task": "farm_alerts.tasks.sync_farm_alert_trackers",
|
||||
"schedule": crontab(
|
||||
minute=FARM_ALERTS_AI_SYNC_CRON_MINUTE,
|
||||
hour=FARM_ALERTS_AI_SYNC_CRON_HOUR,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"standard": {
|
||||
"format": "%(asctime)s %(levelname)s %(name)s %(message)s",
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"farm_ai_assistant_file": {
|
||||
"level": "WARNING",
|
||||
"class": "logging.FileHandler",
|
||||
"filename": LOG_DIR / "farm_ai_assistant.log",
|
||||
"formatter": "standard",
|
||||
},
|
||||
"farm_alerts_file": {
|
||||
"level": "INFO",
|
||||
"class": "logging.FileHandler",
|
||||
"filename": LOG_DIR / "farm_alerts.log",
|
||||
"formatter": "standard",
|
||||
},
|
||||
"external_api_adapter_file": {
|
||||
"level": "WARNING",
|
||||
"class": "logging.FileHandler",
|
||||
"filename": LOG_DIR / "external_api_adapter.log",
|
||||
"formatter": "standard",
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"farm_ai_assistant": {
|
||||
"handlers": ["farm_ai_assistant_file"],
|
||||
"level": "WARNING",
|
||||
"propagate": False,
|
||||
},
|
||||
"farm_alerts": {
|
||||
"handlers": ["farm_alerts_file"],
|
||||
"level": "INFO",
|
||||
"propagate": False,
|
||||
},
|
||||
"external_api_adapter": {
|
||||
"handlers": ["external_api_adapter_file"],
|
||||
"level": "WARNING",
|
||||
"propagate": False,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, inline_serializer
|
||||
|
||||
|
||||
FARM_UUID_DEFAULT = "11111111-1111-1111-1111-111111111111"
|
||||
|
||||
|
||||
class AuthTokenSerializer(serializers.Serializer):
|
||||
token = serializers.CharField()
|
||||
|
||||
|
||||
def code_response(name, data=None, token=False, extra_fields=None):
|
||||
fields = {
|
||||
"code": serializers.IntegerField(),
|
||||
"msg": serializers.CharField(),
|
||||
}
|
||||
if data is not None:
|
||||
fields["data"] = data
|
||||
if token:
|
||||
fields["token"] = serializers.CharField()
|
||||
if extra_fields:
|
||||
fields.update(extra_fields)
|
||||
return inline_serializer(name=name, fields=fields)
|
||||
|
||||
|
||||
def status_response(name, data=None):
|
||||
fields = {
|
||||
"status": serializers.CharField(default="success"),
|
||||
}
|
||||
if data is not None:
|
||||
fields["data"] = data
|
||||
return inline_serializer(name=name, fields=fields)
|
||||
|
||||
|
||||
def farm_uuid_query_param(required=False, description="UUID of the farm."):
|
||||
return OpenApiParameter(
|
||||
name="farm_uuid",
|
||||
type=OpenApiTypes.UUID,
|
||||
location=OpenApiParameter.QUERY,
|
||||
required=required,
|
||||
description=description,
|
||||
default=FARM_UUID_DEFAULT,
|
||||
)
|
||||
|
||||
|
||||
def sensor_uuid_query_param(required=False, description="Optional sensor UUID."):
|
||||
return OpenApiParameter(
|
||||
name="sensor_uuid",
|
||||
type=OpenApiTypes.UUID,
|
||||
location=OpenApiParameter.QUERY,
|
||||
required=required,
|
||||
description=description,
|
||||
)
|
||||
@@ -0,0 +1,44 @@
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
|
||||
path("api/docs/swagger/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"),
|
||||
path("api/docs/redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"),
|
||||
path("api/auth/", include("auth.urls")),
|
||||
path("api/account/", include("account.urls")),
|
||||
path("api/farm-hub/", include("farm_hub.urls")),
|
||||
path("api/access-control/", include("access_control.urls")),
|
||||
path("api/device-hub/", include("device_hub.urls")),
|
||||
path("api/sensor-catalog/", include("device_hub.sensor_catalog_urls")),
|
||||
path("api/farm-dashboard-config/", include("dashboard.urls_config")),
|
||||
path("api/farm-dashboard/", include("dashboard.urls")),
|
||||
path("api/crop-health/", include("crop_health.urls")),
|
||||
path("api/soil/", include("soil.urls")),
|
||||
|
||||
path("api/crop-zoning/", include("crop_zoning.urls")),
|
||||
# path("api/yield-harvest/", include("yield_harvest.urls")),
|
||||
path("api/yield-harvest/", include("yield_harvest.crop_simulation_urls")),
|
||||
|
||||
path("api/pest-detection/", include("pest_detection.urls")),
|
||||
path("api/pest-disease/", include("pest_detection.pest_disease_urls")),
|
||||
path("api/sensor-7-in-1/", include("device_hub.sensor_7_in_1_urls")),
|
||||
path("api/sensors/", include("device_hub.comparison_urls")),
|
||||
path("api/irrigation/", include("irrigation.urls")),
|
||||
|
||||
path("api/weather/", include("water.weather_urls")),
|
||||
path("api/water/", include("water.urls")),
|
||||
path("api/economy/", include("economic_overview.urls")),
|
||||
|
||||
path("api/fertilization/", include("fertilization.urls")),
|
||||
path("api/farm-ai-assistant/", include("farm_ai_assistant.urls")),
|
||||
path("api/notifications/", include("notifications.urls")),
|
||||
path("api/farm-alerts/", include("farm_alerts.urls")),
|
||||
path("api/plants/", include("plants.urls")),
|
||||
path("api/events/", include("farmer_calendar.urls")),
|
||||
path("api/farmer-todos/", include("farmer_todos.urls")),
|
||||
|
||||
path("api/sensor-external-api/", include("device_hub.sensor_external_api_urls")),
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
||||
|
||||
application = get_wsgi_application()
|
||||
Reference in New Issue
Block a user