import datetime, json, os, pytz, random, secrets, contextlib # , uvicorn from enum import Enum #from datetime import datetime, time, timedelta from fastapi import Body, Cookie, Depends, FastAPI, File, Form, Header, HTTPException, Path, Query, Request, Response, status, UploadFile from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, PlainTextResponse from fastapi.staticfiles import StaticFiles from functools import lru_cache from pydantic import BaseModel, EmailStr, Field from typing import Dict, List, Optional, Set, Union from . import config # from app.lib_general import common_route_params, Common_Route_Params import logging import app.log from app.log import setup_logging # Import middleware with alias to avoid shadowing 'app' FastAPI instance from app.middleware import add_process_time_header as process_time_middleware # Centralized router registry from app.routers.registry import setup_routers from app.db_sql import sql_select, reset_redis, reconnect_db from app.lib_config_v3 import bootstrap_db_config, validate_critical_config print('### **** *** ** * The Aether API v4 using FastAPI is loading... * ** *** **** ###') log = logging.getLogger(__name__) # log.setLevel(logging.DEBUG) # DEBUG > INFO > WARNING > ERROR > CRITICAL #logging.basicConfig( #format='[%(asctime)s] %(levelname)s @ %(module)s.%(funcName)s()#%(lineno)d: %(message)s' #) @contextlib.asynccontextmanager async def lifespan(app: FastAPI): """ Handles application startup and shutdown lifecycle. """ # 1. Initialize Logging early but safely setup_logging(config.settings) log.info('### **** *** ** * Aether API v4 using FastAPI - Startup Lifespan Initiated * ** *** **** ###') # 2. Bootstrapping Configuration from DB with robust error handling log.info("Bootstrapping Configuration...") # Save original settings for fallback orig_db_server = config.settings.DB_SERVER orig_db_user = config.settings.DB_USER orig_db_pass = config.settings.DB_PASS orig_db_name = config.settings.DB_NAME orig_db_port = config.settings.DB_PORT try: if bootstrap_db_config(config.settings): log.info("Successfully bootstrapped configuration from database.") # Re-initialize the database engine with new credentials/URI if reconnect_db(): log.info("Database connection re-established with production configuration.") else: log.warning("FAILED to re-establish database connection after bootstrap. Reverting to .env settings.") config.settings.DB_SERVER = orig_db_server config.settings.DB_USER = orig_db_user config.settings.DB_PASS = orig_db_pass config.settings.DB_NAME = orig_db_name config.settings.DB_PORT = orig_db_port reconnect_db() else: log.warning("System bootstrap from DB returned no results. Using environment defaults.") except Exception as e: log.error(f"Unexpected error during configuration bootstrap: {e}. Falling back to .env settings.") config.settings.DB_SERVER = orig_db_server config.settings.DB_USER = orig_db_user config.settings.DB_PASS = orig_db_pass config.settings.DB_NAME = orig_db_name config.settings.DB_PORT = orig_db_port reconnect_db() # 3. Final validation of critical infrastructure validate_critical_config(config.settings) log.info('### **** *** ** * Aether API v4 using FastAPI - Startup Sequence Complete * ** *** **** ###') yield # Shutdown logic log.info('### **** *** ** * Aether API v4 using FastAPI - Shutdown Lifespan Initiated * ** *** **** ###') log.info('The Aether FastAPI API is shutting down...') print('### **** *** ** * Aether API v4 using FastAPI - About to try FastAPI() while loading... * ** *** **** ###') app = FastAPI( # debug = True, title = 'Aether API', description = 'One Sky IT\'s Aether API v4 using FastAPI.', version = '3.00.01', operationsSorter = 'method', lifespan = lifespan, ) # @lru_cache() # def get_settings(): # return config.Settings() app.mount('/static', StaticFiles(directory='static'), name='static') # Register all application routes setup_routers(app) # Updated 2026-02-23 # BEGIN: CORS # NOTE: Eventually this should query the DB for the specific list based on the cfg table and or site_domain table. That way it is dynamic and only allowing those defined in the DB. No wildcards or regex. # NOTE: Need to include .localhost for less browser restrictions! Mainly for audio and video. app.add_middleware( CORSMiddleware, # allow_origins = origins, allow_origins = config.settings.ORIGINS, allow_origin_regex = config.settings.ORIGINS_REGEX, # allow_origin_regex = 'https://.*\.oneskyit\.com', allow_credentials = True, allow_methods = ['*'], allow_headers = ['*'], #expose_headers = [], #max_age = 600, ) # END: CORS # Updated 2026-02-23 # Add middleware to ensure Access-Control-Allow-Private-Network is present # when the response already includes CORS allow-origin (i.e. origin was allowed). @app.middleware("http") async def cors_pna_middleware(request: Request, call_next): """Add `Access-Control-Allow-Private-Network: true` to responses only when CORS has already allowed the request's origin. This avoids echoing PNA for disallowed origins and leverages the existing CORSMiddleware origin validation. """ response = await call_next(request) # Rely on existing CORS logic (CORSMiddleware) to validate origin. # Only add the PNA header if an Allow-Origin header is present. if response.headers.get('access-control-allow-origin') or response.headers.get('Access-Control-Allow-Origin'): response.headers['Access-Control-Allow-Private-Network'] = 'true' return response # Updated 2026-02-23 # Temporary debug middleware: logs Origin and PNA-related request/response headers # Activate only when an Origin header is present to limit log noise. # @app.middleware("http") # async def debug_pna_logging_middleware(request: Request, call_next): # origin = request.headers.get('origin') # if origin: # acrpn = request.headers.get('access-control-request-private-network') # acrm = request.headers.get('access-control-request-method') # log.debug(f"PNA_DEBUG REQ: method={request.method} path={request.url.path} remote={getattr(request.client, 'host', None)} origin={origin} acr_method={acrm} acr_private_network={acrpn}") # response = await call_next(request) # if origin: # # collect CORS/PNA-related response headers for visibility # interesting = {k: v for k, v in response.headers.items() if k.lower().startswith('access-control-') or k.lower() == 'vary'} # log.debug(f"PNA_DEBUG RESP: status={response.status_code} headers={interesting}") # return response # Register utility middleware from external module app.middleware('http')(process_time_middleware) # ### BEGIN ### API Main ### fastapi_root() ### @app.get('/', tags=['Root'], response_class=PlainTextResponse) async def fastapi_root(response: Response = Response): log.setLevel(logging.DEBUG) # DEBUG, INFO, WARN, WARNING, ERROR, EXCEPTION, CRITICAL log.debug(locals()) # log.info(config.settings.APP_NAME) log.info('One Sky IT\'s Aether API root (FastAPI)') log.info('***') log.debug('This is debug') # 10 DEBUG log.info('This is info') # 20 INFO log.warning('This is a warning') # 30 WARNING (and WARN) log.error('This is an error') # 40 ERROR log.exception('This is an exception') # 40 ERROR log.critical('This is critical') # 50 CRITICAL log.info('^^^') log.warning('Resetting Redis...') reset_redis() log.info('Reset Redis') response_data = {} response_data['message'] = 'This is One Sky IT\'s Aether API root (FastAPI).' current_datetime = datetime.datetime.now() current_datetime_string = current_datetime.isoformat() timezone = pytz.timezone("America/New_York") current_datetime_tz = timezone.localize(current_datetime) current_datetime_tz_string = current_datetime_tz.isoformat() current_datetime_utc = datetime.datetime.utcnow() current_datetime_utc_string = current_datetime_utc.isoformat() current_datetime_utc_localize = pytz.utc.localize(current_datetime_utc) current_datetime_utc_localize_string = current_datetime_utc_localize.isoformat() current_datetime_utc_localize_pst = current_datetime_utc_localize.astimezone(pytz.timezone("America/Los_Angeles")) current_datetime_utc_localize_pst_string = current_datetime_utc_localize_pst.isoformat() response_data['datetime'] = current_datetime_string response_data['datetime_tz'] = current_datetime_tz_string response_data['datetime_utc'] = current_datetime_utc_string response_data['datetime_utc_localize'] = current_datetime_utc_localize_string response_data['datetime_utc_localize_pst'] = current_datetime_utc_localize_pst_string response_data['url_safe_string_4_bytes_1'] = secrets.token_urlsafe(4) response_data['url_safe_string_8_bytes_1'] = secrets.token_urlsafe(8) response_data['url_safe_string_8_bytes_2'] = secrets.token_urlsafe(8) response_data['url_safe_string_8_bytes_3'] = secrets.token_urlsafe(8) response_data['url_safe_string_8_bytes_4'] = secrets.token_urlsafe(8) response_data['url_safe_string_8_bytes_5'] = secrets.token_urlsafe(8) response_data['url_safe_string_16_bytes_1'] = secrets.token_urlsafe(16) response_data['url_safe_string_16_bytes_2'] = secrets.token_urlsafe(16) response_data['url_safe_string_16_bytes_3'] = secrets.token_urlsafe(16) response_data['url_safe_string_16_bytes_4'] = secrets.token_urlsafe(16) response_data['url_safe_string_16_bytes_5'] = secrets.token_urlsafe(16) response_data['hex_string_4_bytes_1'] = secrets.token_hex(4) response_data['hex_string_8_bytes_1'] = secrets.token_hex(8) response_data['hex_string_16_bytes_1'] = secrets.token_hex(16) response_data['hex_string_32_bytes_1'] = secrets.token_hex(32) log.debug(json.dumps(response_data, indent=4)) return json.dumps(response_data, indent=4) # , sort_keys=True # ### END ### API Main ### fastapi_root() ### # ### BEGIN ### API Main ### generate_id_random() ### # NOTE: This is just a quick utility function to generate a bunch of random IDs. # Updated 2022-03-30 @app.get('/generate_id_random', tags=['Root'], response_class=PlainTextResponse) async def generate_id_random(response: Response = Response): log.setLevel(logging.INFO) # DEBUG, INFO, WARN, WARNING, ERROR, EXCEPTION, CRITICAL log.debug(locals()) response_data = {} html_list = '