Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion app/app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,14 @@

HOSTNAMES = os.environ.get("DJANGO_HOSTNAMES", "localhost").split(",")
ALLOWED_HOSTS = ["*"]
CORS_ORIGIN_WHITELIST = ["*"]
# CORS: allow configuring allowed origins via env (comma-separated) or accept all with "*"
cors_env = os.environ.get("ASTRO_DASH_CORS_ALLOWED_ORIGINS", "*")
if cors_env.strip() == "*":
CORS_ALLOW_ALL_ORIGINS = True
CORS_ALLOWED_ORIGINS = []
else:
CORS_ALLOW_ALL_ORIGINS = False
CORS_ALLOWED_ORIGINS = [origin.strip() for origin in cors_env.split(",") if origin.strip()]
CSRF_TRUSTED_ORIGINS = ["http://localhost", "http://localhost:8000", "http://localhost:4000"]
for hostname in HOSTNAMES:
CSRF_TRUSTED_ORIGINS.append(f"""https://{hostname}""")
Expand All @@ -58,6 +65,8 @@
"revproxy",
"rest_framework",
"api",
"astrodash",
"corsheaders",
"users",
"django_cron",
"django_filters",
Expand All @@ -69,6 +78,7 @@

MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"corsheaders.middleware.CorsMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
Expand Down
1 change: 1 addition & 0 deletions app/app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
path("admin/", admin.site.urls, name='admin'),
path("", include("host.urls")),
path("api/", include("api.urls")),
path("astrodash/api/v1/", include("astrodash.urls")),
path("", include("users.urls")),
path("oidc/", include("mozilla_django_oidc.urls"))
]
Expand Down
Empty file added app/astrodash/__init__.py
Empty file.
7 changes: 7 additions & 0 deletions app/astrodash/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.apps import AppConfig


class AstroDashConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "astrodash"
verbose_name = "AstroDash Integration"
Empty file.
143 changes: 143 additions & 0 deletions app/astrodash/config/logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import logging
import logging.config
from logging.handlers import RotatingFileHandler
import os
import json
from typing import Optional
from astrodash.config.settings import get_settings, Settings

def get_logger(name: Optional[str] = None) -> logging.Logger:
"""
Get a logger with consistent naming convention.

Args:
name: Logger name. If None, uses the calling module's name.

Returns:
Configured logger instance.
"""
if name is None:
import inspect
frame = inspect.currentframe().f_back
name = frame.f_globals.get('__name__', 'unknown')

return logging.getLogger(name)

def init_logging(config: Optional[Settings] = None) -> None:
"""
Initialize logging configuration for the application.

Args:
config: Settings object containing logging configuration. If None, uses default settings.

Raises:
OSError: If log directory cannot be created or is not writable.
ValueError: If logging configuration is invalid.
"""
try:
config = config or get_settings()
LOG_DIR = config.log_dir
LOG_FILE = os.path.join(LOG_DIR, "app.log")

# Ensure log directory exists
os.makedirs(LOG_DIR, exist_ok=True)

# Verify log directory is writable
if not os.access(LOG_DIR, os.W_OK):
raise OSError(f"Log directory {LOG_DIR} is not writable")

LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S"
},
"json": {
"()": "astrodash.config.logging.JsonFormatter"
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "default",
"level": config.log_level,
},
"file": {
"class": "logging.handlers.RotatingFileHandler",
"formatter": "json",
"filename": LOG_FILE,
"maxBytes": 10 * 1024 * 1024, # 10MB
"backupCount": 5,
"level": config.log_level,
},
},
"root": {
"handlers": ["console", "file"],
"level": config.log_level,
},
"loggers": {
# Configure specific loggers for better control
"uvicorn": {
"level": "INFO",
"handlers": ["console", "file"],
"propagate": False,
},
"uvicorn.access": {
"level": "INFO",
"handlers": ["console", "file"],
"propagate": False,
},
"fastapi": {
"level": "INFO",
"handlers": ["console", "file"],
"propagate": False,
},
}
}

logging.config.dictConfig(LOGGING_CONFIG)

# Log successful initialization
logger = get_logger(__name__)
logger.info(f"Logging initialized successfully. Log level: {config.log_level}, Log file: {LOG_FILE}")

except Exception as e:
# Fallback to basic logging if configuration fails
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
handlers=[
logging.StreamHandler(),
]
)
logging.error(f"Failed to initialize logging configuration: {e}")
raise

class JsonFormatter(logging.Formatter):
"""
Custom JSON formatter for structured logging.
"""

def format(self, record: logging.LogRecord) -> str:
"""Format log record as JSON."""
log_entry = {
"timestamp": self.formatTime(record),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
"module": record.module,
"function": record.funcName,
"line": record.lineno
}

# Add exception info if present
if record.exc_info:
log_entry["exception"] = self.formatException(record.exc_info)

# Add extra fields if present
if hasattr(record, 'extra_fields'):
log_entry.update(record.extra_fields)

return json.dumps(log_entry, ensure_ascii=False)
141 changes: 141 additions & 0 deletions app/astrodash/config/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
from pydantic_settings import BaseSettings
from pydantic import Field, AnyUrl, field_validator
from typing import Optional, List, Dict
import os

class Settings(BaseSettings):
# General
app_name: str = Field("AstroDash API", env="ASTRODASH_APP_NAME")
environment: str = Field("production", env="ASTRODASH_ENVIRONMENT")
debug: bool = Field(False, env="ASTRODASH_DEBUG")

# API
api_prefix: str = Field("/api/v1", env="ASTRODASH_API_PREFIX")
allowed_hosts: List[str] = Field(["*"], env="ASTRODASH_ALLOWED_HOSTS") # Allow all hosts for API usage
cors_origins: List[str] = Field(["*"], env="ASTRODASH_CORS_ORIGINS") # Allow all origins for API usage

# Security Settings
secret_key: str = Field("your-super-secret-key-here-make-it-very-long-and-secure-32-chars-min", env="ASTRODASH_SECRET_KEY")
access_token_expire_minutes: int = Field(60 * 24, env="ASTRODASH_ACCESS_TOKEN_EXPIRE_MINUTES")

# Rate Limiting
rate_limit_requests_per_minute: int = Field(600, env="ASTRODASH_RATE_LIMIT_REQUESTS_PER_MINUTE")
rate_limit_burst_limit: int = Field(100, env="ASTRODASH_RATE_LIMIT_BURST_LIMIT")

# Security Headers
enable_hsts: bool = Field(True, env="ASTRODASH_ENABLE_HSTS")
enable_csp: bool = Field(True, env="ASTRODASH_ENABLE_CSP")
enable_permissions_policy: bool = Field(True, env="ASTRODASH_ENABLE_PERMISSIONS_POLICY")

# Input Validation
max_request_size: int = Field(100 * 1024 * 1024, env="ASTRODASH_MAX_REQUEST_SIZE") # 100MB
max_file_size: int = Field(50 * 1024 * 1024, env="ASTRODASH_MAX_FILE_SIZE") # 50MB

# Session Security
session_cookie_secure: bool = Field(True, env="ASTRODASH_SESSION_COOKIE_SECURE")
session_cookie_httponly: bool = Field(True, env="ASTRODASH_SESSION_COOKIE_HTTPONLY")
session_cookie_samesite: str = Field("strict", env="ASTRODASH_SESSION_COOKIE_SAMESITE")

# Database
db_url: Optional[AnyUrl] = Field(None, env="ASTRODASH_DATABASE_URL")
db_echo: bool = Field(False, env="ASTRODASH_DB_ECHO")

# Data Storage (External to application code)
data_dir: str = Field("/mnt/astrodash-data", env="ASTRODASH_DATA_DIR")
storage_dir: str = Field("/mnt/astrodash-data", env="ASTRODASH_STORAGE_DIR")

# ML Model Paths (External data directory)
user_model_dir: str = Field("/mnt/astrodash-data/user_models", env="ASTRODASH_USER_MODEL_DIR")
dash_model_path: str = Field("/mnt/astrodash-data/pre_trained_models/dash/pytorch_model.pth", env="ASTRODASH_DASH_MODEL_PATH")
dash_training_params_path: str = Field("/mnt/astrodash-data/pre_trained_models/dash/training_params.pickle", env="ASTRODASH_DASH_TRAINING_PARAMS_PATH")
transformer_model_path: str = Field("/mnt/astrodash-data/pre_trained_models/transformer/TF_wiserep_v6.pt", env="ASTRODASH_TRANSFORMER_MODEL_PATH")

# Template and Line List Paths (External data directory)
template_path: str = Field("/mnt/astrodash-data/pre_trained_models/templates/sn_and_host_templates.npz", env="ASTRODASH_TEMPLATE_PATH")
line_list_path: str = Field("/mnt/astrodash-data/pre_trained_models/templates/sneLineList.txt", env="ASTRODASH_LINE_LIST_PATH")

# ML Configuration Parameters
# DASH model parameters
nw: int = Field(1024, env="ASTRODASH_NW") # Number of wavelength bins
w0: float = Field(3500.0, env="ASTRODASH_W0") # Minimum wavelength in Angstroms
w1: float = Field(10000.0, env="ASTRODASH_W1") # Maximum wavelength in Angstroms

# Transformer model parameters
label_mapping: Dict[str, int] = Field(
{'Ia': 0, 'IIn': 1, 'SLSNe-I': 2, 'II': 3, 'Ib/c': 4},
env="ASTRODASH_LABEL_MAPPING"
)

# Transformer architecture parameters
transformer_bottleneck_length: int = Field(1, env="ASTRODASH_TRANSFORMER_BOTTLENECK_LENGTH")
transformer_model_dim: int = Field(128, env="ASTRODASH_TRANSFORMER_MODEL_DIM")
transformer_num_heads: int = Field(4, env="ASTRODASH_TRANSFORMER_NUM_HEADS")
transformer_num_layers: int = Field(6, env="ASTRODASH_TRANSFORMER_NUM_LAYERS")
transformer_ff_dim: int = Field(256, env="ASTRODASH_TRANSFORMER_FF_DIM")
transformer_dropout: float = Field(0.1, env="ASTRODASH_TRANSFORMER_DROPOUT")
transformer_selfattn: bool = Field(False, env="ASTRODASH_TRANSFORMER_SELFATTN")

# User model parameters
user_model_reliability_threshold: float = Field(0.5, env="ASTRODASH_USER_MODEL_RELIABILITY_THRESHOLD")

# Logging
log_dir: str = Field("logs", env="ASTRODASH_LOG_DIR")
log_level: str = Field("INFO", env="ASTRODASH_LOG_LEVEL")

# Other
osc_api_url: str = Field("https://api.astrocats.space", env="ASTRODASH_OSC_API_URL")

class Config:
env_file = ".env"
env_file_encoding = "utf-8"
case_sensitive = True
extra = "allow" # Allow extra fields from environment

@field_validator("allowed_hosts", "cors_origins", mode="before")
@classmethod
def split_str(cls, v):
if isinstance(v, str):
return [i.strip() for i in v.split(",") if i.strip()]
return v

@field_validator("label_mapping", mode="before")
@classmethod
def parse_label_mapping(cls, v):
if isinstance(v, str):
# Parse JSON string if provided as environment variable
import json
try:
return json.loads(v)
except json.JSONDecodeError:
# Fallback to default if parsing fails
return {'Ia': 0, 'IIn': 1, 'SLSNe-I': 2, 'II': 3, 'Ib/c': 4}
return v

@field_validator("secret_key")
@classmethod
def validate_secret_key(cls, v):
if v == "supersecret" and os.getenv("ENVIRONMENT") == "production":
raise ValueError("SECRET_KEY must be set to a secure value in production")
if len(v) < 32:
raise ValueError("SECRET_KEY must be at least 32 characters long")
return v

@field_validator("environment")
@classmethod
def validate_environment(cls, v):
allowed_environments = ["development", "staging", "production", "test"]
if v not in allowed_environments:
raise ValueError(f"Environment must be one of: {allowed_environments}")
return v

@field_validator("session_cookie_samesite")
@classmethod
def validate_session_cookie_samesite(cls, v):
allowed_values = ["strict", "lax", "none"]
if v not in allowed_values:
raise ValueError(f"SESSION_COOKIE_SAMESITE must be one of: {allowed_values}")
return v


def get_settings() -> Settings:
return Settings()
Empty file added app/astrodash/core/__init__.py
Empty file.
Loading