Source code for tidy3d.web.license

"""Helpers for clearing local license authentication state."""

from __future__ import annotations

import shutil
import sys
from pathlib import Path
from typing import TYPE_CHECKING
from uuid import uuid4

if TYPE_CHECKING:
    from collections.abc import Iterator

LICENSE_CACHE_GENERATION_FILE = "td-license-cache-generation"


def _iter_license_auth_files(config_dir: Path) -> Iterator[Path]:
    """Yield persisted license authentication files in a config directory."""

    seen: set[Path] = set()
    candidates = [config_dir / "td.auth", *config_dir.glob("td-*.auth")]
    for path in candidates:
        if path in seen:
            continue
        seen.add(path)
        if path.is_file():
            yield path


def refresh_license_cache(config_dir: str | Path) -> int:
    """Clear all persisted license auth state and bump the process cache generation."""

    config_path = Path(config_dir)
    config_path.mkdir(mode=0o700, parents=True, exist_ok=True)

    staged_dir = config_path / f".td-license-refresh-{uuid4().hex}"
    staged_files: list[tuple[Path, Path]] = []
    auth_files = list(_iter_license_auth_files(config_path))
    if auth_files:
        staged_dir.mkdir(mode=0o700)

    marker_tmp: Path | None = None
    marker_replaced = False
    removed_after_marker = 0
    try:
        for path in auth_files:
            try:
                staged_path = staged_dir / path.name
                path.replace(staged_path)
            except FileNotFoundError:
                continue
            staged_files.append((path, staged_path))

        marker = config_path / LICENSE_CACHE_GENERATION_FILE
        marker_tmp = marker.with_name(f"{marker.name}.{uuid4().hex}.tmp")
        marker_tmp.write_text(f"{uuid4().hex}\n", encoding="utf-8")
        marker_tmp.replace(marker)
        marker_replaced = True

        for path in _iter_license_auth_files(config_path):
            try:
                path.unlink()
            except FileNotFoundError:
                continue
            removed_after_marker += 1

        if removed_after_marker:
            marker_tmp = marker.with_name(f"{marker.name}.{uuid4().hex}.tmp")
            marker_tmp.write_text(f"{uuid4().hex}\n", encoding="utf-8")
            marker_tmp.replace(marker)
    except Exception:
        if not marker_replaced:
            for path, staged_path in reversed(staged_files):
                if staged_path.exists() and not path.exists():
                    staged_path.replace(path)
        if marker_tmp is not None:
            try:
                marker_tmp.unlink()
            except FileNotFoundError:
                pass
        raise
    finally:
        shutil.rmtree(staged_dir, ignore_errors=True)

    return len(staged_files) + removed_after_marker


def reset_failed_tidy3d_extras_import() -> None:
    """Clear a cached failed tidy3d-extras import so the next import retries."""

    module = sys.modules.get("tidy3d_extras")
    if module is None or getattr(module, "extension", None) is not None:
        return

    for name in list(sys.modules):
        if name == "tidy3d_extras" or name.startswith("tidy3d_extras."):
            sys.modules.pop(name, None)


def refresh_license_state(config_dir: str | Path) -> int:
    """Clear all persisted license auth state and failed local-license import state."""

    removed = refresh_license_cache(config_dir)
    reset_failed_tidy3d_extras_import()
    return removed


[docs] def refresh_licenses() -> None: """Clear cached local license entitlements from the active config directory. This does not contact the license server immediately. The next licensed local feature check fetches current entitlements and caches them again. The refresh is cache-wide: it clears all persisted license auth files in this config directory, including files for other API key or endpoint hashes. """ from tidy3d.config import get_manager refresh_license_state(get_manager().config_dir)