From 1f5765dfa1cf30571e4baa838e2aee86e034eb23 Mon Sep 17 00:00:00 2001 From: daquinteroflex Date: Wed, 5 Nov 2025 18:28:14 +0100 Subject: [PATCH] feat: nexus support --- docs/extras/nexus_configuration.rst | 437 +++++++++++++++++++++++++++ tests/config/test_nexus_migration.py | 248 +++++++++++++++ tidy3d/config/legacy.py | 49 ++- tidy3d/config/loader.py | 39 ++- tidy3d/web/cli/app.py | 203 ++++++++++--- tidy3d/web/core/s3utils.py | 30 +- 6 files changed, 960 insertions(+), 46 deletions(-) create mode 100644 docs/extras/nexus_configuration.rst create mode 100644 tests/config/test_nexus_migration.py diff --git a/docs/extras/nexus_configuration.rst b/docs/extras/nexus_configuration.rst new file mode 100644 index 0000000000..575ba9ce8d --- /dev/null +++ b/docs/extras/nexus_configuration.rst @@ -0,0 +1,437 @@ +.. currentmodule:: tidy3d + +Nexus Environment Configuration +================================ + +Overview +-------- + +The Nexus environment enables Tidy3D to connect to custom on-premises or private cloud deployments for enterprise customers. + +.. note:: + This feature is for enterprise customers with custom Tidy3D deployments. Most users should use the standard API key setup with the default production environment. + +Quick Start +----------- + +**Simple setup** using the convenience option: + +.. code-block:: bash + + tidy3d configure --apikey YOUR_KEY \ + --nexus-url http://your-server + +This automatically sets: + +- API endpoint: ``http://your-server/tidy3d-api`` +- Website endpoint: ``http://your-server/tidy3d`` +- S3 endpoint: ``http://your-server:9000`` + +**Advanced setup** with individual endpoints: + +.. code-block:: bash + + tidy3d configure --apikey YOUR_KEY \ + --api-endpoint http://your-server/tidy3d-api \ + --website-endpoint http://your-server/tidy3d + +Or configure only Nexus endpoints (preserves existing API key): + +.. code-block:: bash + + tidy3d configure --nexus-url http://your-server + +Configuration +------------- + +Command Syntax +~~~~~~~~~~~~~~ + +.. code-block:: bash + + tidy3d configure [--apikey ] [--nexus-url | --api-endpoint --website-endpoint ] [OPTIONS] + +**Options:** + +* ``--apikey ``: API key (prompts if not provided and no Nexus options given) +* ``--nexus-url ``: Nexus base URL (automatically sets api=/tidy3d-api, web=/tidy3d, s3=:9000) +* ``--api-endpoint ``: Nexus API server URL (e.g., http://server/tidy3d-api) +* ``--website-endpoint ``: Nexus web interface URL (e.g., http://server/tidy3d) +* ``--s3-region ``: S3 region (default: us-east-1) +* ``--s3-endpoint ``: S3 storage URL (e.g., http://server:9000) +* ``--ssl-verify`` / ``--no-ssl-verify``: SSL verification (default: enabled for HTTPS) +* ``--enable-caching`` / ``--no-caching``: Server-side result caching + +Examples +~~~~~~~~ + +.. code-block:: bash + + # Simple configuration using nexus-url (recommended) + tidy3d configure --apikey YOUR_KEY \ + --nexus-url http://nexus.company.com + + # Configure individual endpoints + tidy3d configure --apikey YOUR_KEY \ + --api-endpoint http://nexus.company.com/tidy3d-api \ + --website-endpoint http://nexus.company.com/tidy3d + + # Add Nexus to existing configuration (preserves API key) + tidy3d configure --nexus-url http://nexus.company.com + + # Full configuration with all options + tidy3d configure --apikey YOUR_KEY \ + --api-endpoint https://api.company.com/tidy3d-api \ + --website-endpoint https://tidy3d.company.com \ + --s3-region eu-west-1 \ + --s3-endpoint https://s3.company.com:9000 \ + --ssl-verify \ + --enable-caching + +Configuration File +~~~~~~~~~~~~~~~~~~ + +**Location:** + +- Linux/macOS: ``~/.config/tidy3d/config.toml`` +- Windows: ``C:\Users\username\.config\tidy3d\config.toml`` +- Legacy: ``~/.tidy3d/config.toml`` (automatically migrated) + +**Format** (new structured TOML): + +.. code-block:: toml + + [web] + apikey = "your-api-key" + api_endpoint = "http://nexus.company.com/tidy3d-api" + website_endpoint = "http://nexus.company.com/tidy3d" + s3_region = "us-east-1" + ssl_verify = false + enable_caching = false + + [web.env_vars] + AWS_ENDPOINT_URL_S3 = "http://nexus.company.com:9000" + +.. note:: + **Automatic Migration:** If you have an existing configuration in the old flat format (``~/.tidy3d/config``), Tidy3D will automatically convert it to the new structured format on first use. Your old configuration will be backed up as ``config.migrated``. + +Legacy Format (deprecated) +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +For reference, the old flat format that is automatically migrated: + +.. code-block:: toml + + # Old format: ~/.tidy3d/config (automatically migrated) + apikey = "your-api-key" + web_api_endpoint = "http://nexus.company.com/tidy3d-api" + website_endpoint = "http://nexus.company.com/tidy3d" + s3_region = "us-east-1" + s3_endpoint = "http://nexus.company.com:9000" + ssl_verify = false + enable_caching = false + +Python Usage +------------ + +No code changes required. Tidy3D automatically uses the configured endpoints: + +.. code-block:: python + + import tidy3d as td + import tidy3d.web as web + + # Works transparently with Nexus configuration + sim = td.Simulation(...) + sim_data = web.run(sim, task_name="my_sim") + +Verifying Configuration +~~~~~~~~~~~~~~~~~~~~~~~~ + +Check your current Nexus configuration: + +.. code-block:: python + + from tidy3d import config + + # Display current configuration + print(config.format()) + + # Check specific web settings + print(f"API Endpoint: {config.web.api_endpoint}") + print(f"Website: {config.web.website_endpoint}") + print(f"S3 Region: {config.web.s3_region}") + print(f"SSL Verify: {config.web.ssl_verify}") + + # Check S3 endpoint (stored in env_vars) + if "AWS_ENDPOINT_URL_S3" in config.web.env_vars: + print(f"S3 Endpoint: {config.web.env_vars['AWS_ENDPOINT_URL_S3']}") + +Programmatic Configuration +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Update Nexus settings from Python: + +.. code-block:: python + + from tidy3d import config + + # Update Nexus endpoints + config.update_section("web", + api_endpoint="http://nexus.company.com/tidy3d-api", + website_endpoint="http://nexus.company.com/tidy3d", + s3_region="us-west-2", + ssl_verify=False, + enable_caching=True + ) + + # Set S3 endpoint + config.update_section("web", + env_vars={"AWS_ENDPOINT_URL_S3": "http://nexus.company.com:9000"} + ) + + # Save changes to disk + config.save() + +Environment Variables +~~~~~~~~~~~~~~~~~~~~~ + +Override any setting temporarily using environment variables: + +.. code-block:: bash + + # Override API endpoint + export TIDY3D_WEB__API_ENDPOINT="http://other-server/tidy3d-api" + + # Override S3 endpoint + export TIDY3D_WEB__ENV_VARS__AWS_ENDPOINT_URL_S3="http://other-s3:9000" + + # Override SSL verification + export TIDY3D_WEB__SSL_VERIFY=false + +Managing Configuration +---------------------- + +Reset to Defaults +~~~~~~~~~~~~~~~~~ + +Reset configuration to default values: + +.. code-block:: bash + + tidy3d config reset + +Or from Python: + +.. code-block:: python + + from tidy3d import config + config.reset_to_defaults() + config.save() + +View Configuration +~~~~~~~~~~~~~~~~~~ + +Display current configuration: + +.. code-block:: bash + + python -c "from tidy3d import config; print(config.format())" + +Or interactively: + +.. code-block:: python + + from tidy3d import config + + # Print formatted configuration + print(config) + + # Or just web section + print(config.web) + +Migrate Legacy Configuration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Manually migrate old configuration to new format: + +.. code-block:: bash + + tidy3d config migrate + +Options: + +* ``--delete-legacy``: Remove old ``~/.tidy3d`` directory after migration +* ``--overwrite``: Overwrite existing new-format configuration + +Remove Configuration +~~~~~~~~~~~~~~~~~~~~ + +To completely remove Nexus configuration: + +.. code-block:: bash + + # Linux/macOS + rm ~/.config/tidy3d/config.toml + + # Or reset to defaults + tidy3d config reset + + # Then reconfigure with standard API key + tidy3d configure --apikey YOUR_API_KEY + +Troubleshooting +--------------- + +Verify Configuration +~~~~~~~~~~~~~~~~~~~~ + +Check that your Nexus configuration is active: + +.. code-block:: python + + from tidy3d import config + + print("=== Tidy3D Configuration ===") + print(f"API Endpoint: {config.web.api_endpoint}") + print(f"Website: {config.web.website_endpoint}") + + # Verify it's not using production + if "simulation.cloud" in str(config.web.api_endpoint): + print("?? Using production, not Nexus!") + else: + print("? Using custom Nexus deployment") + +Test Connectivity +~~~~~~~~~~~~~~~~~ + +Verify your Nexus server is accessible: + +.. code-block:: bash + + # Test API endpoint + curl http://your-nexus-server/tidy3d-api/health + + # Or with Python + python -c "import requests; from tidy3d import config; \ + print(requests.get(f'{config.web.api_endpoint}/health').text)" + +Check S3 Configuration +~~~~~~~~~~~~~~~~~~~~~~ + +Verify S3 endpoint configuration: + +.. code-block:: python + + from tidy3d import config + + if config.web.env_vars and "AWS_ENDPOINT_URL_S3" in config.web.env_vars: + s3_endpoint = config.web.env_vars["AWS_ENDPOINT_URL_S3"] + print(f"S3 Endpoint: {s3_endpoint}") + else: + print("Using default AWS S3") + +Legacy Environment Check (Deprecated) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For backward compatibility, the old Environment API still works: + +.. code-block:: python + + # Old API (deprecated, but still functional) + from tidy3d.web.core.environment import Env + print(f"Environment: {Env.current.name}") + print(f"API Endpoint: {Env.current.web_api_endpoint}") + +.. warning:: + The ``Env`` API is deprecated and will be removed in a future version. Use ``tidy3d.config`` instead. + +Common Issues +~~~~~~~~~~~~~ + +**Issue: Configuration not taking effect** + +Solution: Ensure environment variables aren't overriding your config: + +.. code-block:: bash + + # Check for environment variable overrides + env | grep TIDY3D_WEB + +**Issue: Old config file still exists** + +The old flat config file is automatically backed up during migration. It's safe to delete: + +.. code-block:: bash + + # Old config is backed up as config.migrated + ls ~/.tidy3d/ + # You can safely remove: rm ~/.tidy3d/config.migrated + +**Issue: API key validation fails** + +Ensure you're validating against your Nexus endpoint: + +.. code-block:: bash + + tidy3d configure --apikey YOUR_KEY \ + --nexus-url http://your-nexus-server + +Advanced Topics +--------------- + +Multiple Configurations (Profiles) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Switch between different Nexus deployments: + +.. code-block:: python + + from tidy3d import config + + # Switch to a different profile + config.switch_profile("dev") + + # Configure for dev environment + config.update_section("web", + api_endpoint="http://dev-nexus/tidy3d-api", + website_endpoint="http://dev-nexus/tidy3d" + ) + config.save() + + # Switch back to default + config.switch_profile("default") + +Or via environment variable: + +.. code-block:: bash + + # Use dev profile + export TIDY3D_CONFIG_PROFILE=dev + python your_script.py + +Custom S3 Configuration +~~~~~~~~~~~~~~~~~~~~~~~~ + +For Nexus deployments with MinIO or other S3-compatible storage: + +.. code-block:: python + + from tidy3d import config + + config.update_section("web", + s3_region="us-east-1", + env_vars={ + "AWS_ENDPOINT_URL_S3": "http://minio.company.com:9000", + # Optional: Add other AWS config + "AWS_ACCESS_KEY_ID": "your-key", + "AWS_SECRET_ACCESS_KEY": "your-secret" + } + ) + config.save() + +See Also +-------- + +* :doc:`submit_simulations` - Submitting and managing simulations +* :doc:`../install` - Installation and API key setup diff --git a/tests/config/test_nexus_migration.py b/tests/config/test_nexus_migration.py new file mode 100644 index 0000000000..29c282457f --- /dev/null +++ b/tests/config/test_nexus_migration.py @@ -0,0 +1,248 @@ +"""Tests for Nexus configuration migration from old to new format.""" + +from __future__ import annotations + +import pytest +import toml + +from tidy3d.config.legacy import load_legacy_flat_config +from tidy3d.config.loader import ConfigLoader +from tidy3d.config.manager import ConfigManager + + +@pytest.fixture +def old_nexus_config(tmp_path): + """Create an old-style Nexus configuration.""" + config_dir = tmp_path / ".tidy3d" + config_dir.mkdir() + old_config = config_dir / "config" + + old_config.write_text(""" +apikey = "test-nexus-key" +web_api_endpoint = "http://nexus.company.com:5000" +website_endpoint = "http://nexus.company.com/tidy3d" +s3_region = "us-west-2" +s3_endpoint = "http://nexus.company.com:9000" +ssl_verify = false +enable_caching = true +""") + + return config_dir, old_config + + +@pytest.fixture +def old_minimal_config(tmp_path): + """Create an old-style config with just API key.""" + config_dir = tmp_path / ".tidy3d" + config_dir.mkdir() + old_config = config_dir / "config" + + old_config.write_text('apikey = "test-key"\n') + + return config_dir, old_config + + +def test_load_legacy_nexus_config(old_nexus_config): + """Test that load_legacy_flat_config parses all Nexus fields correctly.""" + config_dir, old_config = old_nexus_config + + legacy_data = load_legacy_flat_config(config_dir) + + assert "web" in legacy_data + web = legacy_data["web"] + + # Check all fields are migrated + assert web["apikey"] == "test-nexus-key" + assert web["api_endpoint"] == "http://nexus.company.com:5000" + assert web["website_endpoint"] == "http://nexus.company.com/tidy3d" + assert web["s3_region"] == "us-west-2" + assert web["ssl_verify"] is False + assert web["enable_caching"] is True + + # S3 endpoint should be in env_vars + assert "env_vars" in web + assert web["env_vars"]["AWS_ENDPOINT_URL_S3"] == "http://nexus.company.com:9000" + + +def test_load_legacy_minimal_config(old_minimal_config): + """Test that minimal old config (just API key) still works.""" + config_dir, old_config = old_minimal_config + + legacy_data = load_legacy_flat_config(config_dir) + + assert "web" in legacy_data + assert legacy_data["web"]["apikey"] == "test-key" + assert len(legacy_data["web"]) == 1 # Only apikey + + +def test_auto_migration_on_load(old_nexus_config): + """Test that ConfigLoader auto-migrates legacy config on first load.""" + config_dir, old_config = old_nexus_config + + # Verify old config exists + assert old_config.exists() + + # Load via ConfigLoader (should trigger auto-migration) + loader = ConfigLoader(config_dir) + data = loader.load_base() + + # New config.toml should be created + new_config = config_dir / "config.toml" + assert new_config.exists() + + # Old config should be backed up + backup = config_dir / "config.migrated" + assert backup.exists() + assert not old_config.exists() + + # Verify data is correct + assert "web" in data + assert data["web"]["api_endpoint"] == "http://nexus.company.com:5000" + + +def test_migrated_config_format(old_nexus_config): + """Test that migrated config is in correct nested TOML format.""" + config_dir, old_config = old_nexus_config + + # Trigger migration + loader = ConfigLoader(config_dir) + loader.load_base() + + # Read the new config file + new_config = config_dir / "config.toml" + parsed = toml.loads(new_config.read_text()) + + # Should have nested structure + assert "web" in parsed + assert "api_endpoint" in parsed["web"] + assert "website_endpoint" in parsed["web"] + assert "env_vars" in parsed["web"] + assert "AWS_ENDPOINT_URL_S3" in parsed["web"]["env_vars"] + + # Old flat keys should NOT exist + assert "web_api_endpoint" not in parsed + assert "s3_endpoint" not in parsed + + +def test_config_manager_with_legacy_nexus(old_nexus_config): + """Test that ConfigManager can load and use legacy Nexus config.""" + config_dir, old_config = old_nexus_config + + manager = ConfigManager(config_dir=config_dir) + + # Should auto-load Nexus settings + web = manager.get_section("web") + + assert str(web.api_endpoint) == "http://nexus.company.com:5000" + assert str(web.website_endpoint) == "http://nexus.company.com/tidy3d" + assert web.s3_region == "us-west-2" + assert web.ssl_verify is False + assert web.enable_caching is True + + # Check env_vars + assert "AWS_ENDPOINT_URL_S3" in web.env_vars + assert web.env_vars["AWS_ENDPOINT_URL_S3"] == "http://nexus.company.com:9000" + + +def test_no_migration_if_new_config_exists(tmp_path): + """Test that migration doesn't happen if config.toml already exists.""" + config_dir = tmp_path / ".tidy3d" + config_dir.mkdir() + + # Create both old and new config + old_config = config_dir / "config" + old_config.write_text('apikey = "old-key"\n') + + new_config = config_dir / "config.toml" + new_config.write_text('[web]\napikey = "new-key"\n') + + # Load - should use new config, not migrate + loader = ConfigLoader(config_dir) + data = loader.load_base() + + # Should have loaded from new config + assert data["web"]["apikey"] == "new-key" + + # Old config should still exist (not backed up) + assert old_config.exists() + backup = config_dir / "config.migrated" + assert not backup.exists() + + +def test_partial_nexus_config(tmp_path): + """Test migration with partial Nexus settings.""" + config_dir = tmp_path / ".tidy3d" + config_dir.mkdir() + old_config = config_dir / "config" + + # Only some Nexus fields + old_config.write_text(""" +apikey = "test-key" +web_api_endpoint = "http://custom:5000" +website_endpoint = "http://custom/web" +""") + + legacy_data = load_legacy_flat_config(config_dir) + + assert legacy_data["web"]["apikey"] == "test-key" + assert legacy_data["web"]["api_endpoint"] == "http://custom:5000" + assert legacy_data["web"]["website_endpoint"] == "http://custom/web" + + # Fields not provided shouldn't be in the dict + assert "s3_region" not in legacy_data["web"] + assert "env_vars" not in legacy_data["web"] + + +def test_save_after_migration(old_nexus_config): + """Test that saving after migration works correctly.""" + config_dir, old_config = old_nexus_config + + manager = ConfigManager(config_dir=config_dir) + + # Modify a setting + manager.update_section("web", timeout=300) + manager.save() + + # Read back the config + new_config = config_dir / "config.toml" + parsed = toml.loads(new_config.read_text()) + + # Should have both migrated and new values + assert parsed["web"]["api_endpoint"] == "http://nexus.company.com:5000" + assert parsed["web"]["timeout"] == 300 + + +def test_no_migration_if_no_legacy_config(tmp_path): + """Test that loader handles missing legacy config gracefully.""" + config_dir = tmp_path / ".tidy3d" + config_dir.mkdir() + + loader = ConfigLoader(config_dir) + data = loader.load_base() + + # Should return empty dict + assert data == {} + + # No files should be created + assert not (config_dir / "config").exists() + assert not (config_dir / "config.toml").exists() + assert not (config_dir / "config.migrated").exists() + + +def test_migration_preserves_comments_when_possible(old_nexus_config): + """Test that migration creates a clean, well-formatted config.""" + config_dir, old_config = old_nexus_config + + # Trigger migration + loader = ConfigLoader(config_dir) + loader.load_base() + + # Read the new config as text + new_config = config_dir / "config.toml" + content = new_config.read_text() + + # Should have section headers + assert "[web]" in content + + # Should not have old flat format + assert "web_api_endpoint" not in content diff --git a/tidy3d/config/legacy.py b/tidy3d/config/legacy.py index 4fb73ddeb4..75b70a1f9e 100644 --- a/tidy3d/config/legacy.py +++ b/tidy3d/config/legacy.py @@ -419,7 +419,20 @@ def _maybe_str(value: Any) -> Optional[str]: def load_legacy_flat_config(config_dir: Path) -> dict[str, Any]: - """Load legacy flat configuration file (pre-migration format).""" + """Load legacy flat configuration file (pre-migration format). + + This function now supports both the original flat config format and + Nexus custom deployment settings introduced in later versions. + + Legacy key mappings: + - apikey -> web.apikey + - web_api_endpoint -> web.api_endpoint + - website_endpoint -> web.website_endpoint + - s3_region -> web.s3_region + - s3_endpoint -> web.env_vars.AWS_ENDPOINT_URL_S3 + - ssl_verify -> web.ssl_verify + - enable_caching -> web.enable_caching + """ legacy_path = config_dir / "config" if not legacy_path.exists(): @@ -438,9 +451,43 @@ def load_legacy_flat_config(config_dir: Path) -> dict[str, Any]: return {} legacy_data: dict[str, Any] = {} + + # Migrate API key (original functionality) apikey = parsed.get("apikey") if apikey is not None: legacy_data.setdefault("web", {})["apikey"] = apikey + + # Migrate Nexus API endpoint + web_api = parsed.get("web_api_endpoint") + if web_api is not None: + legacy_data.setdefault("web", {})["api_endpoint"] = web_api + + # Migrate Nexus website endpoint + website = parsed.get("website_endpoint") + if website is not None: + legacy_data.setdefault("web", {})["website_endpoint"] = website + + # Migrate S3 region + s3_region = parsed.get("s3_region") + if s3_region is not None: + legacy_data.setdefault("web", {})["s3_region"] = s3_region + + # Migrate SSL verification setting + ssl_verify = parsed.get("ssl_verify") + if ssl_verify is not None: + legacy_data.setdefault("web", {})["ssl_verify"] = ssl_verify + + # Migrate caching setting + enable_caching = parsed.get("enable_caching") + if enable_caching is not None: + legacy_data.setdefault("web", {})["enable_caching"] = enable_caching + + # Migrate S3 endpoint to env_vars + s3_endpoint = parsed.get("s3_endpoint") + if s3_endpoint is not None: + env_vars = legacy_data.setdefault("web", {}).setdefault("env_vars", {}) + env_vars["AWS_ENDPOINT_URL_S3"] = s3_endpoint + return legacy_data diff --git a/tidy3d/config/loader.py b/tidy3d/config/loader.py index 0e71210d30..b8a21333d0 100644 --- a/tidy3d/config/loader.py +++ b/tidy3d/config/loader.py @@ -27,15 +27,52 @@ def __init__(self, config_dir: Optional[Path] = None): self._docs: dict[Path, tomlkit.TOMLDocument] = {} def load_base(self) -> dict[str, Any]: - """Load base configuration from config.toml.""" + """Load base configuration from config.toml. + + If config.toml doesn't exist but the legacy flat config does, + automatically migrate to the new format. + """ config_path = self.config_dir / "config.toml" data = self._read_toml(config_path) if data: return data + + # Check for legacy flat config from .legacy import load_legacy_flat_config + legacy_path = self.config_dir / "config" legacy = load_legacy_flat_config(self.config_dir) + + # Auto-migrate if legacy config exists + if legacy and legacy_path.exists(): + log.info( + f"Detected legacy configuration at '{legacy_path}'. " + "Automatically migrating to new format..." + ) + + try: + # Save in new format + self.save_base(legacy) + + # Rename old config to preserve it + backup_path = legacy_path.with_suffix(".migrated") + legacy_path.rename(backup_path) + + log.info( + f"Migration complete. Configuration saved to '{config_path}'. " + f"Legacy config backed up as '{backup_path.name}'." + ) + + # Re-read the newly created config + return self._read_toml(config_path) + except Exception as exc: + log.warning( + f"Failed to auto-migrate legacy configuration: {exc}. " + "Using legacy data without migration." + ) + return legacy + if legacy: return legacy return {} diff --git a/tidy3d/web/cli/app.py b/tidy3d/web/cli/app.py index 173b962ff5..9e37a7e498 100644 --- a/tidy3d/web/cli/app.py +++ b/tidy3d/web/cli/app.py @@ -55,59 +55,194 @@ def tidy3d_cli() -> None: @click.command() -@click.option("--apikey", prompt=False) -def configure(apikey: str) -> None: - """Click command to configure the api key. +@click.option("--apikey", prompt=False, help="Tidy3D API key") +@click.option("--nexus-url", help="Nexus base URL (sets api=/tidy3d-api, web=/tidy3d, s3=:9000)") +@click.option("--api-endpoint", help="Nexus API endpoint URL (e.g., http://server/tidy3d-api)") +@click.option("--website-endpoint", help="Nexus website endpoint URL (e.g., http://server/tidy3d)") +@click.option("--s3-region", help="S3 region (default: us-east-1)") +@click.option("--s3-endpoint", help="S3 endpoint URL (e.g., http://server:9000)") +@click.option("--ssl-verify/--no-ssl-verify", default=None, help="Enable/disable SSL verification") +@click.option( + "--enable-caching/--no-caching", default=None, help="Enable/disable server-side caching" +) +def configure( + apikey: str, + nexus_url: str, + api_endpoint: str, + website_endpoint: str, + s3_region: str, + s3_endpoint: str, + ssl_verify: bool, + enable_caching: bool, +) -> None: + """Configure API key and optionally Nexus environment settings. Parameters ---------- apikey : str - User input api key. + User API key + nexus_url : str + Nexus base URL (automatically derives api/website/s3 endpoints) + api_endpoint : str + Nexus API endpoint URL + website_endpoint : str + Nexus website endpoint URL + s3_region : str + AWS S3 region + s3_endpoint : str + S3 endpoint URL + ssl_verify : bool + Whether to verify SSL certificates + enable_caching : bool + Whether to enable result caching """ - configure_fn(apikey) + configure_fn( + apikey, + nexus_url, + api_endpoint, + website_endpoint, + s3_region, + s3_endpoint, + ssl_verify, + enable_caching, + ) -def configure_fn(apikey: str) -> None: - """Python function that tries to set configuration based on a provided API key. +def configure_fn( + apikey: str | None, + nexus_url: str | None = None, + api_endpoint: str | None = None, + website_endpoint: str | None = None, + s3_region: str | None = None, + s3_endpoint: str | None = None, + ssl_verify: bool | None = None, + enable_caching: bool | None = None, +) -> None: + """Configure API key and optionally Nexus environment settings. Parameters ---------- - apikey : str - User input api key. + apikey : str | None + User API key + nexus_url : str | None + Nexus base URL (automatically derives api/website/s3 endpoints) + api_endpoint : str | None + Nexus API endpoint URL + website_endpoint : str | None + Nexus website endpoint URL + s3_region : str | None + AWS S3 region + s3_endpoint : str | None + S3 endpoint URL + ssl_verify : bool | None + Whether to verify SSL certificates + enable_caching : bool | None + Whether to enable result caching """ - def auth(req: requests.Request) -> requests.Request: - """Enrich auth information to request. - Parameters - ---------- - req : requests.Request - the request needs to add headers for auth. - Returns - ------- - requests.Request - Enriched request. - """ - req.headers[HEADER_APIKEY] = apikey - return req - - if not apikey: + # If nexus_url is provided, derive endpoints from it automatically + if nexus_url: + api_endpoint = f"{nexus_url}/tidy3d-api" + website_endpoint = f"{nexus_url}/tidy3d" + s3_endpoint = f"{nexus_url}:9000" + + # Check if any Nexus options are provided + has_nexus_config = any( + [ + api_endpoint, + website_endpoint, + s3_region, + s3_endpoint, + ssl_verify is not None, + enable_caching is not None, + ] + ) + + # Validate that both endpoints are provided if configuring Nexus + if has_nexus_config and (api_endpoint or website_endpoint): + if not (api_endpoint and website_endpoint): + click.echo( + "Error: Both --api-endpoint and --website-endpoint must be provided together " + "(or use --nexus-url to set both automatically)." + ) + return + + # Handle API key prompt if not provided and no Nexus-only config + if not apikey and not has_nexus_config: current_apikey = get_description() message = f"Current API key: [{current_apikey}]\n" if current_apikey else "" apikey = click.prompt(f"{message}Please enter your api key", type=str) - target_url = config.web.build_api_url("apikey") + # Build updates dictionary for web section + web_updates = {} - try: - resp = requests.get(target_url, auth=auth, verify=config.web.ssl_verify) - except (requests.exceptions.SSLError, ssl.SSLError): - resp = requests.get(target_url, auth=auth, verify=False) + if apikey: + web_updates["apikey"] = apikey + + if api_endpoint: + web_updates["api_endpoint"] = api_endpoint + + if website_endpoint: + web_updates["website_endpoint"] = website_endpoint + + if s3_region is not None: + web_updates["s3_region"] = s3_region - if resp.status_code == 200: - click.echo("Configured successfully.") - config.update_section("web", apikey=apikey) + if ssl_verify is not None: + web_updates["ssl_verify"] = ssl_verify + + if enable_caching is not None: + web_updates["enable_caching"] = enable_caching + + # Handle S3 endpoint via env_vars + if s3_endpoint is not None: + current_env_vars = dict(config.web.env_vars) if config.web.env_vars else {} + current_env_vars["AWS_ENDPOINT_URL_S3"] = s3_endpoint + web_updates["env_vars"] = current_env_vars + + # Validate API key if provided + if apikey: + + def auth(req: requests.Request) -> requests.Request: + """Enrich auth information to request.""" + req.headers[HEADER_APIKEY] = apikey + return req + + # Determine validation endpoint + validation_endpoint = api_endpoint if api_endpoint else str(config.web.api_endpoint) + validation_ssl = ssl_verify if ssl_verify is not None else config.web.ssl_verify + + target_url = f"{validation_endpoint.rstrip('/')}/apikey" + + try: + resp = requests.get(target_url, auth=auth, verify=validation_ssl) + except (requests.exceptions.SSLError, ssl.SSLError): + resp = requests.get(target_url, auth=auth, verify=False) + + if resp.status_code != 200: + click.echo( + f"Error: API key validation failed against endpoint: {validation_endpoint}\n" + f"Status code: {resp.status_code}" + ) + return + + # Apply updates if any + if web_updates: + config.update_section("web", **web_updates) config.save() - else: - click.echo("API key is invalid.") + + if has_nexus_config: + click.echo("Nexus configuration saved successfully.") + if api_endpoint: + click.echo(f" API endpoint: {api_endpoint}") + if website_endpoint: + click.echo(f" Website endpoint: {website_endpoint}") + if s3_endpoint: + click.echo(f" S3 endpoint: {s3_endpoint}") + else: + click.echo("Configuration saved successfully.") + elif not apikey and not has_nexus_config: + click.echo("No configuration changes to apply.") @click.command() diff --git a/tidy3d/web/core/s3utils.py b/tidy3d/web/core/s3utils.py index 6629ba25f2..1dfb3ec1e1 100644 --- a/tidy3d/web/core/s3utils.py +++ b/tidy3d/web/core/s3utils.py @@ -63,16 +63,26 @@ def get_s3_key(self) -> str: return r.path[1:] def get_client(self) -> boto3.client: - """Get the boto client for this token.""" - - return boto3.client( - "s3", - region_name=config.web.s3_region, - aws_access_key_id=self.user_credential.access_key_id, - aws_secret_access_key=self.user_credential.secret_access_key, - aws_session_token=self.user_credential.session_token, - verify=config.web.ssl_verify, - ) + """Get the boto client for this token. + + Automatically configures custom S3 endpoint if specified in web.env_vars. + """ + + client_kwargs = { + "service_name": "s3", + "region_name": config.web.s3_region, + "aws_access_key_id": self.user_credential.access_key_id, + "aws_secret_access_key": self.user_credential.secret_access_key, + "aws_session_token": self.user_credential.session_token, + "verify": config.web.ssl_verify, + } + + # Add custom S3 endpoint if configured (e.g., for Nexus deployments) + if config.web.env_vars and "AWS_ENDPOINT_URL_S3" in config.web.env_vars: + s3_endpoint = config.web.env_vars["AWS_ENDPOINT_URL_S3"] + client_kwargs["endpoint_url"] = s3_endpoint + + return boto3.client(**client_kwargs) def is_expired(self) -> bool: """True if token is expired."""