From 84578a3a188261fffd43ffd08aa3ba053fcc948f Mon Sep 17 00:00:00 2001 From: Akhilesh Negi Date: Thu, 7 Aug 2025 19:59:13 +0530 Subject: [PATCH 01/18] getting rid of unused variables --- backend/app/core/config.py | 4 ---- backend/app/tests/api/routes/test_onboarding.py | 4 +--- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 24779bf3..0c829ed2 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -30,10 +30,6 @@ class Settings(BaseSettings): env_ignore_empty=True, extra="ignore", ) - LANGFUSE_PUBLIC_KEY: str - LANGFUSE_SECRET_KEY: str - LANGFUSE_HOST: str # 🇪🇺 EU region - OPENAI_API_KEY: str API_V1_STR: str = "/api/v1" SECRET_KEY: str = secrets.token_urlsafe(32) # 60 minutes * 24 hours * 1 days = 1 days diff --git a/backend/app/tests/api/routes/test_onboarding.py b/backend/app/tests/api/routes/test_onboarding.py index 6f032700..11c7ca55 100644 --- a/backend/app/tests/api/routes/test_onboarding.py +++ b/backend/app/tests/api/routes/test_onboarding.py @@ -1,11 +1,9 @@ -import pytest from fastapi.testclient import TestClient from app.main import app # Assuming your FastAPI app is in app/main.py from app.models import Organization, Project, User, APIKey from app.crud import create_organization, create_project, create_user, create_api_key -from app.api.deps import SessionDep from sqlalchemy import create_engine -from sqlmodel import Session, SQLModel +from sqlmodel import Session from app.core.config import settings from app.tests.utils.utils import random_email, random_lower_string from app.core.security import decrypt_api_key From b63ff7bda7549ffb14805c3fe380d7fade36a111 Mon Sep 17 00:00:00 2001 From: Akhilesh Negi Date: Thu, 7 Aug 2025 19:59:18 +0530 Subject: [PATCH 02/18] getting rid of unused variables --- .env.example | 11 ----------- .github/workflows/benchmark.yml | 4 ---- .github/workflows/continuous_integration.yml | 6 ++++-- .gitignore | 2 +- docker-compose.yml | 8 -------- 5 files changed, 5 insertions(+), 26 deletions(-) diff --git a/.env.example b/.env.example index abea3f77..b5ca0ba9 100644 --- a/.env.example +++ b/.env.example @@ -44,14 +44,3 @@ AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= AWS_DEFAULT_REGION=ap-south-1 AWS_S3_BUCKET_PREFIX="bucket-prefix-name" - -# OpenAI - -OPENAI_API_KEY="this_is_not_a_secret" -LANGFUSE_PUBLIC_KEY="this_is_not_a_secret" -LANGFUSE_SECRET_KEY="this_is_not_a_secret" -LANGFUSE_HOST="this_is_not_a_secret" - -# Misc - -CI="" diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 917203f2..daccfd6e 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -18,10 +18,6 @@ jobs: count: [100] env: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - LANGFUSE_PUBLIC_KEY: ${{ secrets.LANGFUSE_PUBLIC_KEY }} - LANGFUSE_SECRET_KEY: ${{ secrets.LANGFUSE_SECRET_KEY }} - LANGFUSE_HOST: ${{ secrets.LANGFUSE_HOST }} LOCAL_CREDENTIALS_ORG_OPENAI_API_KEY: ${{ secrets.LOCAL_CREDENTIALS_ORG_OPENAI_API_KEY }} LOCAL_CREDENTIALS_API_KEY: ${{ secrets.LOCAL_CREDENTIALS_API_KEY }} diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 5b40cdd3..b617c59e 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -15,7 +15,7 @@ jobs: env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres - POSTGRES_DB: ai_platform + POSTGRES_DB: ai_platform_test ports: - 5432:5432 options: --health-cmd "pg_isready -U postgres" --health-interval 10s --health-timeout 5s --health-retries 5 @@ -34,7 +34,9 @@ jobs: python-version: ${{ matrix.python-version }} - name: Making env file - run: cp .env.example .env + run: | + cp .env.test.example .env + cp .env.test.example .env.test - name: Install uv uses: astral-sh/setup-uv@v6 diff --git a/.gitignore b/.gitignore index 0d8e46df..ad2127f4 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ node_modules/ /playwright/.cache/ # Environments -.env +.env* .venv env/ venv/ diff --git a/docker-compose.yml b/docker-compose.yml index e8e1330d..78a4af52 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -72,10 +72,6 @@ services: - POSTGRES_USER=${POSTGRES_USER?Variable not set} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD?Variable not set} - SENTRY_DSN=${SENTRY_DSN} - - OPENAI_API_KEY=${OPENAI_API_KEY} - - LANGFUSE_PUBLIC_KEY=${LANGFUSE_PUBLIC_KEY} - - LANGFUSE_SECRET_KEY=${LANGFUSE_SECRET_KEY} - - LANGFUSE_HOST=${LANGFUSE_HOST} - LOCAL_CREDENTIALS_ORG_OPENAI_API_KEY=${LOCAL_CREDENTIALS_ORG_OPENAI_API_KEY} - LOCAL_CREDENTIALS_API_KEY=${LOCAL_CREDENTIALS_API_KEY} - EMAIL_TEST_USER=${EMAIL_TEST_USER} @@ -112,10 +108,6 @@ services: - POSTGRES_USER=${POSTGRES_USER?Variable not set} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD?Variable not set} - SENTRY_DSN=${SENTRY_DSN} - - OPENAI_API_KEY=${OPENAI_API_KEY} - - LANGFUSE_PUBLIC_KEY=${LANGFUSE_PUBLIC_KEY} - - LANGFUSE_SECRET_KEY=${LANGFUSE_SECRET_KEY} - - LANGFUSE_HOST=${LANGFUSE_HOST} - LOCAL_CREDENTIALS_ORG_OPENAI_API_KEY=${LOCAL_CREDENTIALS_ORG_OPENAI_API_KEY} - LOCAL_CREDENTIALS_API_KEY=${LOCAL_CREDENTIALS_API_KEY} - EMAIL_TEST_USER=${EMAIL_TEST_USER} From ac7a1360cd9722570ffc85afab2fb36d8745c2c0 Mon Sep 17 00:00:00 2001 From: Akhilesh Negi Date: Thu, 7 Aug 2025 22:53:00 +0530 Subject: [PATCH 03/18] loading variables --- backend/app/api/main.py | 2 +- backend/app/core/config.py | 75 +++++++++++++++++++----- backend/app/load_env.py | 13 ++++ backend/app/main.py | 10 +++- backend/app/tests/api/routes/test_org.py | 14 +++++ backend/app/tests/conftest.py | 10 ++++ backend/test_env.py | 17 ++++++ 7 files changed, 123 insertions(+), 18 deletions(-) create mode 100644 backend/app/load_env.py create mode 100644 backend/test_env.py diff --git a/backend/app/api/main.py b/backend/app/api/main.py index df0b1016..b148c3d6 100644 --- a/backend/app/api/main.py +++ b/backend/app/api/main.py @@ -37,5 +37,5 @@ api_router.include_router(users.router) api_router.include_router(utils.router) -if settings.ENVIRONMENT == "local": +if settings.ENVIRONMENT == "development": api_router.include_router(private.router) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 0c829ed2..f4035dc4 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -1,7 +1,7 @@ import secrets import warnings import os -from typing import Annotated, Any, Literal +from typing import Any, Literal from pydantic import ( EmailStr, @@ -15,35 +15,62 @@ from typing_extensions import Self -def parse_cors(v: Any) -> list[str] | str: - if isinstance(v, str) and not v.startswith("["): - return [i.strip() for i in v.split(",")] - elif isinstance(v, list | str): - return v - raise ValueError(v) +class Settings(BaseSettings): + env: str = "development" # Default value, will be overridden in __init__ + + def __init__(self, **kwargs): + # Determine env_file based on current environment at instantiation time + env = os.getenv("APP_ENV", "development") + # Use absolute path to ensure the file is found correctly + # config.py is in backend/app/core/, so we need to go up 3 levels to reach project root + base_dir = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.dirname(__file__))) + ) + env_file = ( + os.path.join(base_dir, ".env.test") + if env == "testing" + else os.path.join(base_dir, ".env") + ) + # Load the environment file manually to ensure it's loaded correctly + if os.path.exists(env_file): + from dotenv import load_dotenv + + load_dotenv(env_file, override=True) + + # Pass env as a keyword argument to override the default + kwargs["env"] = env + super().__init__(**kwargs) -class Settings(BaseSettings): model_config = SettingsConfigDict( - # Use top level .env file (one level above ./backend/) - env_file="../.env", env_ignore_empty=True, extra="ignore", ) + API_V1_STR: str = "/api/v1" SECRET_KEY: str = secrets.token_urlsafe(32) # 60 minutes * 24 hours * 1 days = 1 days ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 1 - ENVIRONMENT: Literal["local", "staging", "production"] = "local" + + @computed_field # type: ignore[prop-decorator] + @property + def ENV(self) -> Literal["testing", "development", "staging", "production"]: + return self.env # type: ignore PROJECT_NAME: str - SENTRY_DSN: HttpUrl | None = None + SENTRY_DSN: str = "" POSTGRES_SERVER: str POSTGRES_PORT: int = 5432 POSTGRES_USER: str POSTGRES_PASSWORD: str = "" POSTGRES_DB: str = "" + # Additional fields from .env files + ENVIRONMENT: str = "development" # This is different from the env field + STACK_NAME: str = "" + DOCKER_IMAGE_BACKEND: str = "" + DOCKER_IMAGE_FRONTEND: str = "" + @computed_field # type: ignore[prop-decorator] @property def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn: @@ -70,7 +97,7 @@ def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn: @computed_field # type: ignore[prop-decorator] @property def AWS_S3_BUCKET(self) -> str: - return f"{self.AWS_S3_BUCKET_PREFIX}-{self.ENVIRONMENT}" + return f"{self.AWS_S3_BUCKET_PREFIX}-{self.ENV}" LOG_DIR: str = os.path.join(os.path.dirname(os.path.dirname(__file__)), "logs") @@ -80,7 +107,7 @@ def _check_default_secret(self, var_name: str, value: str | None) -> None: f'The value of {var_name} is "changethis", ' "for security, please change it, at least for deployments." ) - if self.ENVIRONMENT == "local": + if self.ENV == "development": warnings.warn(message, stacklevel=1) else: raise ValueError(message) @@ -96,4 +123,22 @@ def _enforce_non_default_secrets(self) -> Self: return self -settings = Settings() # type: ignore +class SettingsSingleton: + _instance: Settings | None = None + + def __new__(cls): + if cls._instance is None: + cls._instance = Settings() + return cls._instance + + def __getattr__(self, name): + return getattr(self._instance, name) + + @classmethod + def reset(cls): + """Reset the singleton instance to force recreation.""" + cls._instance = None + + +# Create settings instance with lazy loading +settings = SettingsSingleton() # type: ignore diff --git a/backend/app/load_env.py b/backend/app/load_env.py new file mode 100644 index 00000000..eaf68b4d --- /dev/null +++ b/backend/app/load_env.py @@ -0,0 +1,13 @@ +import os +from dotenv import load_dotenv + + +def load_environment(): + env = os.getenv("APP_ENV", "development") + + # Use the same path as config.py expects (one level above backend/) + env_file = "../.env" + if env == "testing": + env_file = "../.env.test" + + load_dotenv(env_file) diff --git a/backend/app/main.py b/backend/app/main.py index 903e02ef..0f48ec37 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,19 +1,25 @@ +import os import sentry_sdk + from fastapi import FastAPI from fastapi.routing import APIRoute from asgi_correlation_id.middleware import CorrelationIdMiddleware from app.api.main import api_router from app.core.config import settings -import app.core.logger from app.core.exception_handlers import register_exception_handlers from app.core.middleware import http_request_logger +from app.load_env import load_environment + +# Load environment variables +load_environment() + def custom_generate_unique_id(route: APIRoute) -> str: return f"{route.tags[0]}-{route.name}" -if settings.SENTRY_DSN and settings.ENVIRONMENT != "local": +if settings.SENTRY_DSN and settings.ENVIRONMENT != "development": sentry_sdk.init(dsn=str(settings.SENTRY_DSN), enable_tracing=True) app = FastAPI( diff --git a/backend/app/tests/api/routes/test_org.py b/backend/app/tests/api/routes/test_org.py index 2b518f34..564c5859 100644 --- a/backend/app/tests/api/routes/test_org.py +++ b/backend/app/tests/api/routes/test_org.py @@ -16,6 +16,20 @@ def test_organization(db: Session): return create_test_organization(db) +import os + + +def test_secret_key_env(): + # Get the value of the SECRET_KEY environment variable + secret_key = os.getenv("POSTGRES_DB") + + # Print the value of SECRET_KEY + print(f"SECRET_KEY: {secret_key}") + + # Optionally, you can assert it if needed + assert secret_key == "hello" + + # Test creating an organization def test_create_organization(db: Session, superuser_token_headers: dict[str, str]): org_name = "Test-Org" diff --git a/backend/app/tests/conftest.py b/backend/app/tests/conftest.py index 02773871..573b3200 100644 --- a/backend/app/tests/conftest.py +++ b/backend/app/tests/conftest.py @@ -1,4 +1,5 @@ import pytest +import os from fastapi.testclient import TestClient from sqlmodel import Session from sqlalchemy import event @@ -14,6 +15,15 @@ from app.seed_data.seed_data import seed_database +def pytest_configure(): + os.environ.setdefault("APP_ENV", "testing") + # Force reload of settings after setting environment + import app.core.config + + app.core.config.SettingsSingleton.reset() + app.core.config.settings = app.core.config.SettingsSingleton() + + @pytest.fixture(scope="function") def db() -> Generator[Session, None, None]: connection = engine.connect() diff --git a/backend/test_env.py b/backend/test_env.py new file mode 100644 index 00000000..2a1e7720 --- /dev/null +++ b/backend/test_env.py @@ -0,0 +1,17 @@ +from pydantic_settings import BaseSettings +import os + + +class TestSettings(BaseSettings): + SECRET_KEY: str = "default" + + model_config = { + "env_file": "/Users/akhileshnegi/Projects/ai-platform/.env.test", + "extra": "ignore", + } + + +if __name__ == "__main__": + os.environ["APP_ENV"] = "testing" + s = TestSettings() + print(f"SECRET_KEY: {s.SECRET_KEY}") From 473391974f5abea52620df5f7f4076ac8059c40f Mon Sep 17 00:00:00 2001 From: Akhilesh Negi Date: Thu, 7 Aug 2025 23:02:23 +0530 Subject: [PATCH 04/18] loading engine --- backend/app/core/db.py | 13 +++++++++++-- backend/app/tests/api/routes/test_org.py | 11 ++++------- backend/app/tests/conftest.py | 6 +++++- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/backend/app/core/db.py b/backend/app/core/db.py index ba991fb3..4d24e700 100644 --- a/backend/app/core/db.py +++ b/backend/app/core/db.py @@ -1,10 +1,19 @@ from sqlmodel import Session, create_engine, select from app import crud -from app.core.config import settings from app.models import User, UserCreate -engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI)) + +def get_engine(): + """Get database engine with current settings.""" + # Import settings dynamically to get the current instance + from app.core.config import settings + + return create_engine(str(settings.SQLALCHEMY_DATABASE_URI)) + + +# Create a default engine for backward compatibility +engine = get_engine() # make sure all SQLModel models are imported (app.models) before initializing DB diff --git a/backend/app/tests/api/routes/test_org.py b/backend/app/tests/api/routes/test_org.py index 564c5859..39db0dbf 100644 --- a/backend/app/tests/api/routes/test_org.py +++ b/backend/app/tests/api/routes/test_org.py @@ -20,14 +20,11 @@ def test_organization(db: Session): def test_secret_key_env(): - # Get the value of the SECRET_KEY environment variable - secret_key = os.getenv("POSTGRES_DB") + # Get the value of the POSTGRES_DB environment variable + postgres_db = os.getenv("POSTGRES_DB") - # Print the value of SECRET_KEY - print(f"SECRET_KEY: {secret_key}") - - # Optionally, you can assert it if needed - assert secret_key == "hello" + # Verify it's using the test database + assert postgres_db == "ai_platform_test" # Test creating an organization diff --git a/backend/app/tests/conftest.py b/backend/app/tests/conftest.py index 573b3200..53abcb14 100644 --- a/backend/app/tests/conftest.py +++ b/backend/app/tests/conftest.py @@ -6,7 +6,7 @@ from collections.abc import Generator from app.core.config import settings -from app.core.db import engine +from app.core.db import get_engine from app.api.deps import get_db from app.main import app from app.models import APIKeyPublic @@ -26,6 +26,8 @@ def pytest_configure(): @pytest.fixture(scope="function") def db() -> Generator[Session, None, None]: + # Use dynamic engine to get the correct database + engine = get_engine() connection = engine.connect() transaction = connection.begin() session = Session(bind=connection) @@ -47,6 +49,8 @@ def restart_savepoint(sess, trans): @pytest.fixture(scope="session", autouse=True) def seed_baseline(): + # Use dynamic engine to get the correct database + engine = get_engine() with Session(engine) as session: seed_database(session) # deterministic baseline yield From 25ad418ebc1339d607e8ce9fa69fbc46ac28e6c4 Mon Sep 17 00:00:00 2001 From: Akhilesh Negi Date: Thu, 7 Aug 2025 23:03:50 +0530 Subject: [PATCH 05/18] creating more pools --- backend/app/core/db.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/backend/app/core/db.py b/backend/app/core/db.py index 4d24e700..510e9da7 100644 --- a/backend/app/core/db.py +++ b/backend/app/core/db.py @@ -9,7 +9,18 @@ def get_engine(): # Import settings dynamically to get the current instance from app.core.config import settings - return create_engine(str(settings.SQLALCHEMY_DATABASE_URI)) + # Configure connection pool settings + # For testing, we need more connections since tests run in parallel + pool_size = 20 if settings.ENVIRONMENT == "development" else 5 + max_overflow = 30 if settings.ENVIRONMENT == "development" else 10 + + return create_engine( + str(settings.SQLALCHEMY_DATABASE_URI), + pool_size=pool_size, + max_overflow=max_overflow, + pool_pre_ping=True, + pool_recycle=300, # Recycle connections after 5 minutes + ) # Create a default engine for backward compatibility From 460b36705b7061d371a4e0dc45fe05957e6caf63 Mon Sep 17 00:00:00 2001 From: Akhilesh Negi Date: Fri, 8 Aug 2025 09:56:29 +0530 Subject: [PATCH 06/18] adding example --- .env.test.example | 25 +++++++++++++++++++++++++ .gitignore | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 .env.test.example diff --git a/.env.test.example b/.env.test.example new file mode 100644 index 00000000..90c3747d --- /dev/null +++ b/.env.test.example @@ -0,0 +1,25 @@ +ENVIRONMENT=testing + +PROJECT_NAME="AI Platform" +STACK_NAME=ai-platform + +#Backend +SECRET_KEY=changethis +FIRST_SUPERUSER=superuser@example.com +FIRST_SUPERUSER_PASSWORD=changethis +EMAIL_TEST_USER="test@example.com" + +# Postgres + +POSTGRES_SERVER=localhost +POSTGRES_PORT=5432 +POSTGRES_DB=ai_platform_test +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres + +# AWS + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=ap-south-1 +AWS_S3_BUCKET_PREFIX="bucket-prefix-name" diff --git a/.gitignore b/.gitignore index ad2127f4..0d8e46df 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ node_modules/ /playwright/.cache/ # Environments -.env* +.env .venv env/ venv/ From 9eb8c5ad224fc93a9ea60d8f4b4c6033f2a43f2b Mon Sep 17 00:00:00 2001 From: Akhilesh Negi Date: Fri, 8 Aug 2025 09:57:33 +0530 Subject: [PATCH 07/18] updating gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0d8e46df..ad2127f4 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ node_modules/ /playwright/.cache/ # Environments -.env +.env* .venv env/ venv/ From 69278b50ea8ac5b78938e7685c8016eb4fcc6140 Mon Sep 17 00:00:00 2001 From: Akhilesh Negi Date: Fri, 8 Aug 2025 10:29:45 +0530 Subject: [PATCH 08/18] cleanup variables --- backend/app/tests/conftest.py | 2 +- backend/test_env.py | 17 ----------------- 2 files changed, 1 insertion(+), 18 deletions(-) delete mode 100644 backend/test_env.py diff --git a/backend/app/tests/conftest.py b/backend/app/tests/conftest.py index 53abcb14..80cb03aa 100644 --- a/backend/app/tests/conftest.py +++ b/backend/app/tests/conftest.py @@ -16,7 +16,7 @@ def pytest_configure(): - os.environ.setdefault("APP_ENV", "testing") + os.environ.setdefault("ENVIRONMENT", "testing") # Force reload of settings after setting environment import app.core.config diff --git a/backend/test_env.py b/backend/test_env.py deleted file mode 100644 index 2a1e7720..00000000 --- a/backend/test_env.py +++ /dev/null @@ -1,17 +0,0 @@ -from pydantic_settings import BaseSettings -import os - - -class TestSettings(BaseSettings): - SECRET_KEY: str = "default" - - model_config = { - "env_file": "/Users/akhileshnegi/Projects/ai-platform/.env.test", - "extra": "ignore", - } - - -if __name__ == "__main__": - os.environ["APP_ENV"] = "testing" - s = TestSettings() - print(f"SECRET_KEY: {s.SECRET_KEY}") From 89e80d400d2f57722d11656be47dadbf4477dbf0 Mon Sep 17 00:00:00 2001 From: Akhilesh Negi Date: Fri, 8 Aug 2025 10:33:12 +0530 Subject: [PATCH 09/18] cleanups --- backend/app/core/config.py | 7 +++---- backend/app/load_env.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index f4035dc4..ba730f0f 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -1,11 +1,10 @@ +import os import secrets import warnings -import os -from typing import Any, Literal +from typing import Literal from pydantic import ( EmailStr, - HttpUrl, PostgresDsn, computed_field, model_validator, @@ -20,7 +19,7 @@ class Settings(BaseSettings): def __init__(self, **kwargs): # Determine env_file based on current environment at instantiation time - env = os.getenv("APP_ENV", "development") + env = os.getenv("ENVIRONMENT", "development") # Use absolute path to ensure the file is found correctly # config.py is in backend/app/core/, so we need to go up 3 levels to reach project root base_dir = os.path.dirname( diff --git a/backend/app/load_env.py b/backend/app/load_env.py index eaf68b4d..f465b23b 100644 --- a/backend/app/load_env.py +++ b/backend/app/load_env.py @@ -3,7 +3,7 @@ def load_environment(): - env = os.getenv("APP_ENV", "development") + env = os.getenv("ENVIRONMENT", "development") # Use the same path as config.py expects (one level above backend/) env_file = "../.env" From 6a829f507b6f44e6cb8eb9a1e0507318c54046d2 Mon Sep 17 00:00:00 2001 From: Akhilesh Negi Date: Fri, 8 Aug 2025 10:38:12 +0530 Subject: [PATCH 10/18] cleanups --- backend/app/main.py | 1 - backend/app/tests/api/routes/test_org.py | 11 ----------- 2 files changed, 12 deletions(-) diff --git a/backend/app/main.py b/backend/app/main.py index 0f48ec37..0b473484 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,4 +1,3 @@ -import os import sentry_sdk from fastapi import FastAPI diff --git a/backend/app/tests/api/routes/test_org.py b/backend/app/tests/api/routes/test_org.py index 39db0dbf..2b518f34 100644 --- a/backend/app/tests/api/routes/test_org.py +++ b/backend/app/tests/api/routes/test_org.py @@ -16,17 +16,6 @@ def test_organization(db: Session): return create_test_organization(db) -import os - - -def test_secret_key_env(): - # Get the value of the POSTGRES_DB environment variable - postgres_db = os.getenv("POSTGRES_DB") - - # Verify it's using the test database - assert postgres_db == "ai_platform_test" - - # Test creating an organization def test_create_organization(db: Session, superuser_token_headers: dict[str, str]): org_name = "Test-Org" From 4d0c11af9900865db5d1dc4ffa419a2b28012e81 Mon Sep 17 00:00:00 2001 From: Akhilesh Negi Date: Fri, 8 Aug 2025 10:52:11 +0530 Subject: [PATCH 11/18] skipping warning in development and testing --- backend/app/core/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index ba730f0f..308955b1 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -106,7 +106,7 @@ def _check_default_secret(self, var_name: str, value: str | None) -> None: f'The value of {var_name} is "changethis", ' "for security, please change it, at least for deployments." ) - if self.ENV == "development": + if self.ENV == ["development", "testing"]: warnings.warn(message, stacklevel=1) else: raise ValueError(message) From 84dcee6345025921dd930f390029eebc4087d762 Mon Sep 17 00:00:00 2001 From: Akhilesh Negi Date: Fri, 8 Aug 2025 11:55:01 +0530 Subject: [PATCH 12/18] skipping for testing and development --- backend/app/core/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 308955b1..cf3ca468 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -106,7 +106,7 @@ def _check_default_secret(self, var_name: str, value: str | None) -> None: f'The value of {var_name} is "changethis", ' "for security, please change it, at least for deployments." ) - if self.ENV == ["development", "testing"]: + if self.ENV in ["development", "testing"]: warnings.warn(message, stacklevel=1) else: raise ValueError(message) From cd7bc8a33fb3318984fcbbf953916ac3ca6e531a Mon Sep 17 00:00:00 2001 From: Akhilesh Negi Date: Fri, 8 Aug 2025 11:59:35 +0530 Subject: [PATCH 13/18] updating default keys --- .env.test.example | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.test.example b/.env.test.example index 90c3747d..16860524 100644 --- a/.env.test.example +++ b/.env.test.example @@ -19,7 +19,7 @@ POSTGRES_PASSWORD=postgres # AWS -AWS_ACCESS_KEY_ID= -AWS_SECRET_ACCESS_KEY= +AWS_ACCESS_KEY_ID=this_is_a_test_key +AWS_SECRET_ACCESS_KEY=this_is_a_test_key AWS_DEFAULT_REGION=ap-south-1 AWS_S3_BUCKET_PREFIX="bucket-prefix-name" From d204f1acbde52894d9eb14d871c112c93345e58e Mon Sep 17 00:00:00 2001 From: Akhilesh Negi Date: Fri, 8 Aug 2025 12:18:33 +0530 Subject: [PATCH 14/18] updating default keys --- .env.test.example | 5 +++++ backend/app/api/main.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.env.test.example b/.env.test.example index 16860524..9065e4e6 100644 --- a/.env.test.example +++ b/.env.test.example @@ -17,6 +17,11 @@ POSTGRES_DB=ai_platform_test POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres +# Configure these with your own Docker registry images + +DOCKER_IMAGE_BACKEND=backend +DOCKER_IMAGE_FRONTEND=frontend + # AWS AWS_ACCESS_KEY_ID=this_is_a_test_key diff --git a/backend/app/api/main.py b/backend/app/api/main.py index b148c3d6..9d3ae489 100644 --- a/backend/app/api/main.py +++ b/backend/app/api/main.py @@ -37,5 +37,5 @@ api_router.include_router(users.router) api_router.include_router(utils.router) -if settings.ENVIRONMENT == "development": +if settings.ENVIRONMENT in ["development", "testing"]: api_router.include_router(private.router) From c4cf1e7a872f228e93553313305d2a20ebe2372f Mon Sep 17 00:00:00 2001 From: AkhileshNegi Date: Wed, 20 Aug 2025 23:49:10 +0530 Subject: [PATCH 15/18] WIP towards using get_setting --- backend/app/core/config.py | 85 ++++++++++++---------------------- backend/app/tests/conftest.py | 20 +++----- backend/scripts/tests-start.sh | 3 ++ 3 files changed, 38 insertions(+), 70 deletions(-) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index cf3ca468..51b7c700 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -1,10 +1,12 @@ -import os import secrets import warnings +import os +from typing import Annotated, Any, Literal +from functools import lru_cache -from typing import Literal from pydantic import ( EmailStr, + HttpUrl, PostgresDsn, computed_field, model_validator, @@ -14,34 +16,17 @@ from typing_extensions import Self -class Settings(BaseSettings): - env: str = "development" # Default value, will be overridden in __init__ - - def __init__(self, **kwargs): - # Determine env_file based on current environment at instantiation time - env = os.getenv("ENVIRONMENT", "development") - # Use absolute path to ensure the file is found correctly - # config.py is in backend/app/core/, so we need to go up 3 levels to reach project root - base_dir = os.path.dirname( - os.path.dirname(os.path.dirname(os.path.dirname(__file__))) - ) - env_file = ( - os.path.join(base_dir, ".env.test") - if env == "testing" - else os.path.join(base_dir, ".env") - ) - - # Load the environment file manually to ensure it's loaded correctly - if os.path.exists(env_file): - from dotenv import load_dotenv - - load_dotenv(env_file, override=True) +def parse_cors(v: Any) -> list[str] | str: + if isinstance(v, str) and not v.startswith("["): + return [i.strip() for i in v.split(",")] + elif isinstance(v, list | str): + return v + raise ValueError(v) - # Pass env as a keyword argument to override the default - kwargs["env"] = env - super().__init__(**kwargs) +class Settings(BaseSettings): model_config = SettingsConfigDict( + # env_file will be set dynamically in get_settings() env_ignore_empty=True, extra="ignore", ) @@ -50,26 +35,18 @@ def __init__(self, **kwargs): SECRET_KEY: str = secrets.token_urlsafe(32) # 60 minutes * 24 hours * 1 days = 1 days ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 1 - - @computed_field # type: ignore[prop-decorator] - @property - def ENV(self) -> Literal["testing", "development", "staging", "production"]: - return self.env # type: ignore + ENVIRONMENT: Literal[ + "development", "testing", "staging", "production" + ] = "development" PROJECT_NAME: str - SENTRY_DSN: str = "" + SENTRY_DSN: HttpUrl | None = None POSTGRES_SERVER: str POSTGRES_PORT: int = 5432 POSTGRES_USER: str POSTGRES_PASSWORD: str = "" POSTGRES_DB: str = "" - # Additional fields from .env files - ENVIRONMENT: str = "development" # This is different from the env field - STACK_NAME: str = "" - DOCKER_IMAGE_BACKEND: str = "" - DOCKER_IMAGE_FRONTEND: str = "" - @computed_field # type: ignore[prop-decorator] @property def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn: @@ -96,7 +73,7 @@ def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn: @computed_field # type: ignore[prop-decorator] @property def AWS_S3_BUCKET(self) -> str: - return f"{self.AWS_S3_BUCKET_PREFIX}-{self.ENV}" + return f"{self.AWS_S3_BUCKET_PREFIX}-{self.ENVIRONMENT}" LOG_DIR: str = os.path.join(os.path.dirname(os.path.dirname(__file__)), "logs") @@ -106,7 +83,7 @@ def _check_default_secret(self, var_name: str, value: str | None) -> None: f'The value of {var_name} is "changethis", ' "for security, please change it, at least for deployments." ) - if self.ENV in ["development", "testing"]: + if self.ENVIRONMENT in ["development", "testing"]: warnings.warn(message, stacklevel=1) else: raise ValueError(message) @@ -122,22 +99,18 @@ def _enforce_non_default_secrets(self) -> Self: return self -class SettingsSingleton: - _instance: Settings | None = None - - def __new__(cls): - if cls._instance is None: - cls._instance = Settings() - return cls._instance +@lru_cache() +def get_settings() -> Settings: + """Get settings with appropriate env file based on ENVIRONMENT.""" + environment = os.getenv("ENVIRONMENT", "development") - def __getattr__(self, name): - return getattr(self._instance, name) + # Determine env file + env_files = {"testing": "../.env.test", "development": "../.env"} + env_file = env_files.get(environment, "../.env") - @classmethod - def reset(cls): - """Reset the singleton instance to force recreation.""" - cls._instance = None + # Create Settings instance with the appropriate env file + return Settings(_env_file=env_file) -# Create settings instance with lazy loading -settings = SettingsSingleton() # type: ignore +# Export settings instance +settings = get_settings() diff --git a/backend/app/tests/conftest.py b/backend/app/tests/conftest.py index 80cb03aa..954059e7 100644 --- a/backend/app/tests/conftest.py +++ b/backend/app/tests/conftest.py @@ -1,12 +1,17 @@ import pytest import os + +# Set environment before importing ANYTHING else +os.environ["ENVIRONMENT"] = "testing" + from fastapi.testclient import TestClient from sqlmodel import Session from sqlalchemy import event from collections.abc import Generator +# Now import after setting environment from app.core.config import settings -from app.core.db import get_engine +from app.core.db import engine from app.api.deps import get_db from app.main import app from app.models import APIKeyPublic @@ -15,19 +20,8 @@ from app.seed_data.seed_data import seed_database -def pytest_configure(): - os.environ.setdefault("ENVIRONMENT", "testing") - # Force reload of settings after setting environment - import app.core.config - - app.core.config.SettingsSingleton.reset() - app.core.config.settings = app.core.config.SettingsSingleton() - - @pytest.fixture(scope="function") def db() -> Generator[Session, None, None]: - # Use dynamic engine to get the correct database - engine = get_engine() connection = engine.connect() transaction = connection.begin() session = Session(bind=connection) @@ -49,8 +43,6 @@ def restart_savepoint(sess, trans): @pytest.fixture(scope="session", autouse=True) def seed_baseline(): - # Use dynamic engine to get the correct database - engine = get_engine() with Session(engine) as session: seed_database(session) # deterministic baseline yield diff --git a/backend/scripts/tests-start.sh b/backend/scripts/tests-start.sh index 89dcb0da..0b118512 100644 --- a/backend/scripts/tests-start.sh +++ b/backend/scripts/tests-start.sh @@ -2,6 +2,9 @@ set -e set -x +# Set environment for testing +export ENVIRONMENT=testing + python app/tests_pre_start.py bash scripts/test.sh "$@" From 19567f374de2ef3adafddb15b404f4fa2e1cd31f Mon Sep 17 00:00:00 2001 From: AkhileshNegi Date: Wed, 20 Aug 2025 23:54:42 +0530 Subject: [PATCH 16/18] updating script --- backend/scripts/tests-start.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/backend/scripts/tests-start.sh b/backend/scripts/tests-start.sh index 0b118512..1f6cc798 100644 --- a/backend/scripts/tests-start.sh +++ b/backend/scripts/tests-start.sh @@ -7,4 +7,11 @@ export ENVIRONMENT=testing python app/tests_pre_start.py +# Run pending migrations for test database +uv run alembic upgrade head +if [ $? -ne 0 ]; then + echo 'Error: Test database migrations failed' + exit 1 +fi + bash scripts/test.sh "$@" From eaa661eb01b73e33b268e87583a8fc89d25b15f1 Mon Sep 17 00:00:00 2001 From: AkhileshNegi Date: Mon, 25 Aug 2025 11:20:46 +0530 Subject: [PATCH 17/18] removing LRU cache --- backend/app/core/config.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 51b7c700..41a84231 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -1,8 +1,7 @@ import secrets import warnings import os -from typing import Annotated, Any, Literal -from functools import lru_cache +from typing import Any, Literal from pydantic import ( EmailStr, @@ -99,7 +98,6 @@ def _enforce_non_default_secrets(self) -> Self: return self -@lru_cache() def get_settings() -> Settings: """Get settings with appropriate env file based on ENVIRONMENT.""" environment = os.getenv("ENVIRONMENT", "development") From 046221dbfc493ded9f3d81d517fc203e8e40c4a9 Mon Sep 17 00:00:00 2001 From: AkhileshNegi Date: Mon, 25 Aug 2025 11:51:20 +0530 Subject: [PATCH 18/18] renaming variables --- backend/app/core/config.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 41a84231..4af13cb6 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -15,12 +15,14 @@ from typing_extensions import Self -def parse_cors(v: Any) -> list[str] | str: - if isinstance(v, str) and not v.startswith("["): - return [i.strip() for i in v.split(",")] - elif isinstance(v, list | str): - return v - raise ValueError(v) +def parse_cors(origins: Any) -> list[str] | str: + # If it's a plain comma-separated string, split it into a list + if isinstance(origins, str) and not origins.startswith("["): + return [origin.strip() for origin in origins.split(",")] + # If it's already a list or JSON-style string, just return it + elif isinstance(origins, (list, str)): + return origins + raise ValueError(f"Invalid CORS origins format: {origins!r}") class Settings(BaseSettings):