diff --git a/src/launch_client.py b/src/launch_client.py index 05825a2a..20b4cac8 100644 --- a/src/launch_client.py +++ b/src/launch_client.py @@ -22,7 +22,7 @@ logger = logging_config.logging.getLogger("launch_client") # Import launch_server if it exists -LAUNCH_SERVER_EXISTS = True +launch_server_exists = True try: from launch_server import start_server, get_api_key @@ -31,7 +31,7 @@ except ImportError as ex: logger.debug("API Server not present: %s", ex) os.environ.pop("API_SERVER_CONTROL", None) - LAUNCH_SERVER_EXISTS = False + launch_server_exists = False BASE_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -159,7 +159,7 @@ def main() -> None: if __name__ == "__main__": # Start Server if not running init_server_state() - if LAUNCH_SERVER_EXISTS: + if launch_server_exists: try: logger.debug("Server PID: %i", state.server["pid"]) except KeyError: diff --git a/src/server/api/core/bootstrap.py b/src/server/api/core/bootstrap.py deleted file mode 100644 index fd970758..00000000 --- a/src/server/api/core/bootstrap.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -Copyright (c) 2024, 2025, Oracle and/or its affiliates. -Licensed under the Universal Permissive License v1.0 as shown at http://oss.oracle.com/licenses/upl. -""" -# spell-checker:ignore genai - -from server.bootstrap import databases, models, oci, prompts, settings -from common import logging_config - -logger = logging_config.logging.getLogger("api.core.bootstrap") - -DATABASE_OBJECTS = databases.main() -MODEL_OBJECTS = models.main() -OCI_OBJECTS = oci.main() -PROMPT_OBJECTS = prompts.main() -SETTINGS_OBJECTS = settings.main() diff --git a/src/server/api/core/databases.py b/src/server/api/core/databases.py index 4919ff24..e74ec78b 100644 --- a/src/server/api/core/databases.py +++ b/src/server/api/core/databases.py @@ -4,7 +4,7 @@ """ from typing import Optional, Union -from server.api.core import bootstrap +from server.bootstrap import bootstrap from common.schema import Database, DatabaseNameType from common import logging_config diff --git a/src/server/api/core/prompts.py b/src/server/api/core/prompts.py index 78409376..57a70a8b 100644 --- a/src/server/api/core/prompts.py +++ b/src/server/api/core/prompts.py @@ -5,7 +5,7 @@ # spell-checker:ignore from typing import Optional, Union -from server.api.core import bootstrap +from server.bootstrap import bootstrap from common.schema import PromptCategoryType, PromptNameType, Prompt from common import logging_config diff --git a/src/server/api/core/settings.py b/src/server/api/core/settings.py index 0f993721..bc89de5a 100644 --- a/src/server/api/core/settings.py +++ b/src/server/api/core/settings.py @@ -6,7 +6,7 @@ import os import copy import json -from server.api.core import bootstrap +from server.bootstrap import bootstrap from common.schema import Settings, Configuration, ClientIdType from common import logging_config diff --git a/src/server/api/utils/databases.py b/src/server/api/utils/databases.py index cf6d6011..3b85da05 100644 --- a/src/server/api/utils/databases.py +++ b/src/server/api/utils/databases.py @@ -156,7 +156,7 @@ def connect(config: Database) -> oracledb.Connection: raise except OSError as ex: raise ConnectionError(f"Error connecting to database: {ex}") from ex - + logger.debug("Connected to Databases: %s", config.dsn) return conn diff --git a/src/server/api/utils/oci.py b/src/server/api/utils/oci.py index e9cd87d7..bc0510c7 100644 --- a/src/server/api/utils/oci.py +++ b/src/server/api/utils/oci.py @@ -12,7 +12,7 @@ import oci -from server.bootstrap.bootstrap import OCI_OBJECTS, SETTINGS_OBJECTS +from server.bootstrap import bootstrap from common.schema import OracleCloudSettings, ClientIdType, OCIProfileType from common import logging_config @@ -48,12 +48,19 @@ def get( if client is not None and auth_profile is not None: raise ValueError("provide either 'client' or 'auth_profile', not both") - oci_objects = OCI_OBJECTS + oci_objects = bootstrap.OCI_OBJECTS if client is not None: # Get client settings directly from SETTINGS_OBJECTS - client_settings = next((s for s in SETTINGS_OBJECTS if s.client == client), None) + logger.debug("Looking for client %s in SETTINGS_OBJECTS", client) + logger.debug( + "SETTINGS_OBJECTS has %d entries: %s", + len(bootstrap.SETTINGS_OBJECTS), + [s.client for s in bootstrap.SETTINGS_OBJECTS], + ) + client_settings = next((s for s in bootstrap.SETTINGS_OBJECTS if s.client == client), None) if not client_settings: - raise ValueError(f"client {client} not found") + available_clients = [s.client for s in bootstrap.SETTINGS_OBJECTS] + raise ValueError(f"client {client} not found in SETTINGS_OBJECTS with clients: {available_clients}") derived_auth_profile = ( getattr(client_settings.oci, "auth_profile", "DEFAULT") if client_settings.oci else "DEFAULT" @@ -275,6 +282,7 @@ def get_genai_models(config: OracleCloudSettings, regional: bool = False) -> lis "id": model.id, } ) + logger.info("Registered %i GenAI Models", len(genai_models)) except oci.exceptions.ServiceError as ex: logger.info("Unable to get GenAI Models in Region: %s (%s)", region["region_name"], ex.message) except (oci.exceptions.RequestException, urllib3.exceptions.MaxRetryError): diff --git a/src/server/bootstrap/bootstrap.py b/src/server/bootstrap/bootstrap.py index 9f5e2e4f..da05592b 100644 --- a/src/server/bootstrap/bootstrap.py +++ b/src/server/bootstrap/bootstrap.py @@ -4,11 +4,13 @@ """ # spell-checker:ignore genai -from server.bootstrap import models, oci, settings +from server.bootstrap import databases, models, oci, prompts, settings from common import logging_config logger = logging_config.logging.getLogger("bootstrap") +DATABASE_OBJECTS = databases.main() MODEL_OBJECTS = models.main() OCI_OBJECTS = oci.main() +PROMPT_OBJECTS = prompts.main() SETTINGS_OBJECTS = settings.main() diff --git a/src/server/bootstrap/oci.py b/src/server/bootstrap/oci.py index c40c297f..81cb3c3d 100644 --- a/src/server/bootstrap/oci.py +++ b/src/server/bootstrap/oci.py @@ -16,6 +16,49 @@ logger = logging_config.logging.getLogger("bootstrap.oci") +def _apply_env_overrides_to_default_profile(config: list[dict]) -> None: + """Apply environment variable overrides to the DEFAULT OCI profile.""" + def override(profile: dict, key: str, env_key: str, env: dict, overrides: dict, default=None): + val = env.get(env_key) + if val is not None and val != profile.get(key): + overrides[key] = (profile.get(key), val) + return val + return profile.get(key, default) + + env = os.environ + + for profile in config: + if profile["auth_profile"] == oci.config.DEFAULT_PROFILE: + overrides = {} + + profile.update( + { + "tenancy": override(profile, "tenancy", "OCI_CLI_TENANCY", env, overrides), + "region": override(profile, "region", "OCI_CLI_REGION", env, overrides), + "user": override(profile, "user", "OCI_CLI_USER", env, overrides), + "fingerprint": override(profile, "fingerprint", "OCI_CLI_FINGERPRINT", env, overrides), + "key_file": override(profile, "key_file", "OCI_CLI_KEY_FILE", env, overrides), + "security_token_file": override( + profile, "security_token_file", "OCI_CLI_SECURITY_TOKEN_FILE", env, overrides + ), + "authentication": env.get("OCI_CLI_AUTH") + or ("security_token" if profile.get("security_token_file") else "api_key"), + "genai_compartment_id": override( + profile, "genai_compartment_id", "OCI_GENAI_COMPARTMENT_ID", env, overrides, None + ), + "genai_region": override(profile, "genai_region", "OCI_GENAI_REGION", env, overrides, None), + "log_requests": profile.get("log_requests", False), + "additional_user_agent": profile.get("additional_user_agent", ""), + "pass_phrase": profile.get("pass_phrase"), + } + ) + + if overrides: + logger.info("Environment variable overrides for OCI DEFAULT profile:") + for key, (old, new) in overrides.items(): + logger.info(" %s: '%s' -> '%s'", key, old, new) + + def main() -> list[OracleCloudSettings]: """Read in OCI Configuration options into an object""" logger.debug("*** Bootstrapping OCI - Start") @@ -62,45 +105,7 @@ def main() -> list[OracleCloudSettings]: config.append({"auth_profile": oci.config.DEFAULT_PROFILE}) # Override DEFAULT profile with environment variables - def override(profile: dict, key: str, env_key: str, env: dict, overrides: dict, default=None): - val = env.get(env_key) - if val is not None and val != profile.get(key): - overrides[key] = (profile.get(key), val) - return val - return profile.get(key, default) - - env = os.environ - - for profile in config: - if profile["auth_profile"] == oci.config.DEFAULT_PROFILE: - overrides = {} - - profile.update( - { - "tenancy": override(profile, "tenancy", "OCI_CLI_TENANCY", env, overrides), - "region": override(profile, "region", "OCI_CLI_REGION", env, overrides), - "user": override(profile, "user", "OCI_CLI_USER", env, overrides), - "fingerprint": override(profile, "fingerprint", "OCI_CLI_FINGERPRINT", env, overrides), - "key_file": override(profile, "key_file", "OCI_CLI_KEY_FILE", env, overrides), - "security_token_file": override( - profile, "security_token_file", "OCI_CLI_SECURITY_TOKEN_FILE", env, overrides - ), - "authentication": env.get("OCI_CLI_AUTH") - or ("security_token" if profile.get("security_token_file") else "api_key"), - "genai_compartment_id": override( - profile, "genai_compartment_id", "OCI_GENAI_COMPARTMENT_ID", env, overrides, None - ), - "genai_region": override(profile, "genai_region", "OCI_GENAI_REGION", env, overrides, None), - "log_requests": profile.get("log_requests", False), - "additional_user_agent": profile.get("additional_user_agent", ""), - "pass_phrase": profile.get("pass_phrase"), - } - ) - - if overrides: - logger.info("Environment variable overrides for OCI DEFAULT profile:") - for key, (old, new) in overrides.items(): - logger.info(" %s: '%s' -> '%s'", key, old, new) + _apply_env_overrides_to_default_profile(config) # Build final OracleCloudSettings objects oci_objects = [] diff --git a/tests/server/unit/api/core/test_core_databases.py b/tests/server/unit/api/core/test_core_databases.py index e3e9deca..9aacd384 100644 --- a/tests/server/unit/api/core/test_core_databases.py +++ b/tests/server/unit/api/core/test_core_databases.py @@ -9,7 +9,7 @@ import pytest from server.api.core import databases -from server.api.core import bootstrap +from server.bootstrap import bootstrap from common.schema import Database diff --git a/tests/server/unit/api/utils/test_utils_databases.py b/tests/server/unit/api/utils/test_utils_databases.py index 9161e788..c6db7c1d 100644 --- a/tests/server/unit/api/utils/test_utils_databases.py +++ b/tests/server/unit/api/utils/test_utils_databases.py @@ -14,7 +14,7 @@ from server.api.utils import databases from server.api.utils.databases import DbException -from server.api.core import bootstrap +from server.bootstrap import bootstrap from common.schema import Database diff --git a/tests/server/unit/test_utils_models.py b/tests/server/unit/api/utils/test_utils_models.py similarity index 100% rename from tests/server/unit/test_utils_models.py rename to tests/server/unit/api/utils/test_utils_models.py diff --git a/tests/server/unit/api/utils/test_utils_oci.py b/tests/server/unit/api/utils/test_utils_oci.py index d4475a64..c15ec5c6 100644 --- a/tests/server/unit/api/utils/test_utils_oci.py +++ b/tests/server/unit/api/utils/test_utils_oci.py @@ -38,13 +38,13 @@ def setup_method(self): ) self.sample_client_settings = Settings(client="test_client", oci=OciSettings(auth_profile="CUSTOM")) - @patch("server.api.utils.oci.OCI_OBJECTS", []) + @patch("server.bootstrap.bootstrap.OCI_OBJECTS", []) def test_get_no_objects_configured(self): """Test getting OCI settings when none are configured""" with pytest.raises(ValueError, match="not configured"): oci_utils.get() - @patch("server.api.utils.oci.OCI_OBJECTS", new_callable=list) + @patch("server.bootstrap.bootstrap.OCI_OBJECTS", new_callable=list) def test_get_all(self, mock_oci_objects): """Test getting all OCI settings when no filters are provided""" all_oci = [self.sample_oci_default, self.sample_oci_custom] @@ -54,7 +54,7 @@ def test_get_all(self, mock_oci_objects): assert result == all_oci - @patch("server.api.utils.oci.OCI_OBJECTS") + @patch("server.bootstrap.bootstrap.OCI_OBJECTS") def test_get_by_auth_profile_found(self, mock_oci_objects): """Test getting OCI settings by auth_profile when it exists""" mock_oci_objects.__iter__ = MagicMock(return_value=iter([self.sample_oci_default, self.sample_oci_custom])) @@ -63,7 +63,7 @@ def test_get_by_auth_profile_found(self, mock_oci_objects): assert result == self.sample_oci_custom - @patch("server.api.utils.oci.OCI_OBJECTS") + @patch("server.bootstrap.bootstrap.OCI_OBJECTS") def test_get_by_auth_profile_not_found(self, mock_oci_objects): """Test getting OCI settings by auth_profile when it doesn't exist""" mock_oci_objects.__iter__ = MagicMock(return_value=iter([self.sample_oci_default])) @@ -71,31 +71,52 @@ def test_get_by_auth_profile_not_found(self, mock_oci_objects): with pytest.raises(ValueError, match="profile 'NONEXISTENT' not found"): oci_utils.get(auth_profile="NONEXISTENT") - @patch("server.api.utils.oci.OCI_OBJECTS") - @patch("server.api.utils.oci.SETTINGS_OBJECTS") - def test_get_by_client_with_oci_settings(self, mock_settings_objects, mock_oci_objects): + def test_get_by_client_with_oci_settings(self): """Test getting OCI settings by client when client has OCI settings""" - mock_settings_objects.__iter__ = MagicMock(return_value=iter([self.sample_client_settings])) - mock_oci_objects.__iter__ = MagicMock(return_value=iter([self.sample_oci_default, self.sample_oci_custom])) + from server.bootstrap import bootstrap - result = oci_utils.get(client="test_client") + # Save originals + orig_settings = bootstrap.SETTINGS_OBJECTS + orig_oci = bootstrap.OCI_OBJECTS - assert result == self.sample_oci_custom + try: + # Replace with test data + bootstrap.SETTINGS_OBJECTS = [self.sample_client_settings] + bootstrap.OCI_OBJECTS = [self.sample_oci_default, self.sample_oci_custom] + + result = oci_utils.get(client="test_client") - @patch("server.api.utils.oci.OCI_OBJECTS") - @patch("server.api.utils.oci.SETTINGS_OBJECTS") - def test_get_by_client_without_oci_settings(self, mock_settings_objects, mock_oci_objects): + assert result == self.sample_oci_custom + finally: + # Restore originals + bootstrap.SETTINGS_OBJECTS = orig_settings + bootstrap.OCI_OBJECTS = orig_oci + + def test_get_by_client_without_oci_settings(self): """Test getting OCI settings by client when client has no OCI settings""" + from server.bootstrap import bootstrap + client_settings_no_oci = Settings(client="test_client", oci=None) - mock_settings_objects.__iter__ = MagicMock(return_value=iter([client_settings_no_oci])) - mock_oci_objects.__iter__ = MagicMock(return_value=iter([self.sample_oci_default])) - result = oci_utils.get(client="test_client") + # Save originals + orig_settings = bootstrap.SETTINGS_OBJECTS + orig_oci = bootstrap.OCI_OBJECTS + + try: + # Replace with test data + bootstrap.SETTINGS_OBJECTS = [client_settings_no_oci] + bootstrap.OCI_OBJECTS = [self.sample_oci_default] + + result = oci_utils.get(client="test_client") - assert result == self.sample_oci_default + assert result == self.sample_oci_default + finally: + # Restore originals + bootstrap.SETTINGS_OBJECTS = orig_settings + bootstrap.OCI_OBJECTS = orig_oci - @patch("server.api.utils.oci.OCI_OBJECTS") - @patch("server.api.utils.oci.SETTINGS_OBJECTS") + @patch("server.bootstrap.bootstrap.OCI_OBJECTS") + @patch("server.bootstrap.bootstrap.SETTINGS_OBJECTS") def test_get_by_client_not_found(self, mock_settings_objects, mock_oci_objects): """Test getting OCI settings when client doesn't exist""" mock_settings_objects.__iter__ = MagicMock(return_value=iter([])) @@ -103,15 +124,25 @@ def test_get_by_client_not_found(self, mock_settings_objects, mock_oci_objects): with pytest.raises(ValueError, match="client test_client not found"): oci_utils.get(client="test_client") - @patch("server.api.utils.oci.OCI_OBJECTS") - @patch("server.api.utils.oci.SETTINGS_OBJECTS") - def test_get_by_client_no_matching_profile(self, mock_settings_objects, mock_oci_objects): + def test_get_by_client_no_matching_profile(self): """Test getting OCI settings by client when no matching profile exists""" - mock_settings_objects.__iter__ = MagicMock(return_value=iter([self.sample_client_settings])) - mock_oci_objects.__iter__ = MagicMock(return_value=iter([self.sample_oci_default])) # Only DEFAULT profile - - with pytest.raises(ValueError, match="No settings found for client 'test_client' with auth_profile 'CUSTOM'"): - oci_utils.get(client="test_client") + from server.bootstrap import bootstrap + + # Save originals + orig_settings = bootstrap.SETTINGS_OBJECTS + orig_oci = bootstrap.OCI_OBJECTS + + try: + # Replace with test data + bootstrap.SETTINGS_OBJECTS = [self.sample_client_settings] + bootstrap.OCI_OBJECTS = [self.sample_oci_default] # Only DEFAULT profile + + with pytest.raises(ValueError, match="No settings found for client 'test_client' with auth_profile 'CUSTOM'"): + oci_utils.get(client="test_client") + finally: + # Restore originals + bootstrap.SETTINGS_OBJECTS = orig_settings + bootstrap.OCI_OBJECTS = orig_oci def test_get_both_client_and_auth_profile(self): """Test that providing both client and auth_profile raises an error""" diff --git a/tests/server/unit/api/core/test_core_bootstrap.py b/tests/server/unit/bootstrap/test_bootstrap.py similarity index 94% rename from tests/server/unit/api/core/test_core_bootstrap.py rename to tests/server/unit/bootstrap/test_bootstrap.py index 090e99d4..2f1c6d42 100644 --- a/tests/server/unit/api/core/test_core_bootstrap.py +++ b/tests/server/unit/bootstrap/test_bootstrap.py @@ -7,7 +7,7 @@ import importlib from unittest.mock import patch, MagicMock -from server.api.core import bootstrap +from server.bootstrap import bootstrap class TestBootstrap: @@ -50,4 +50,4 @@ def test_module_imports_and_initialization( def test_logger_exists(self): """Test that logger is properly configured""" assert hasattr(bootstrap, "logger") - assert bootstrap.logger.name == "api.core.bootstrap" + assert bootstrap.logger.name == "bootstrap"