Source code for ionworks.urls

"""Frontend URL helpers for the Ionworks web app.

This module provides the :class:`UrlsClient`, which builds links to pages in
the Ionworks web app without requiring callers to hand-construct URLs from
entity IDs.

Every method takes the resource's own ID plus whatever parent IDs the route
requires, with ``project_id`` resolved from the explicit argument, else the
project_id configured on the :class:`~ionworks.Ionworks` client (which itself
comes from the ``IONWORKS_PROJECT_ID`` env var).

The web-app host is derived from the client's ``api_url`` so links point at the
same environment the client talks to (``api.ionworks.com`` →
``app.ionworks.com``, ``stage-api.ionworks.com`` → ``stage.ionworks.com``,
etc.). Set ``IONWORKS_APP_URL`` to override — needed for local development,
where the frontend runs on a different port than the API and so cannot be
derived.
"""

from __future__ import annotations

import os
import re
from typing import TYPE_CHECKING
from urllib.parse import urlparse

from ._project_id import resolve_project_id

if TYPE_CHECKING:
    from .client import Ionworks


_DEFAULT_APP_BASE_URL = "https://app.ionworks.com"


def _derive_app_base_url(api_url: str | None) -> str:
    """Map an API base URL to its corresponding web-app base URL.

    Parameters
    ----------
    api_url : str | None
        The API base URL the client is configured with (e.g.
        ``https://api.ionworks.com``).

    Returns
    -------
    str
        The web-app base URL for the same environment. Production
        (``api.ionworks.com``) maps to ``https://app.ionworks.com``;
        environment-prefixed API hosts (``{env}-api.ionworks.com``, e.g.
        internal and stage) map to ``https://{env}.ionworks.com``. Any other
        host — localhost, opaque Porter hosts, custom domains — falls back to
        the production app URL, since the frontend host is not derivable from
        it (set ``IONWORKS_APP_URL`` to override).
    """
    if not api_url:
        return _DEFAULT_APP_BASE_URL
    host = urlparse(api_url).hostname or ""
    if host == "api.ionworks.com":
        return _DEFAULT_APP_BASE_URL
    match = re.fullmatch(r"([a-z0-9-]+)-api\.ionworks\.com", host)
    if match:
        return f"https://{match.group(1)}.ionworks.com"
    return _DEFAULT_APP_BASE_URL


[docs] class UrlsClient: """Build links to pages in the Ionworks web app. Returned URLs point at the environment the client is configured for, derived from its ``api_url`` (override with ``IONWORKS_APP_URL``). """
[docs] def __init__(self, client: Ionworks) -> None: self._client = client
@property def _app_base_url(self) -> str: """The web-app base URL for the client's environment. Uses the ``IONWORKS_APP_URL`` env var when set, otherwise derives the host from the client's ``api_url``. Returns ------- str The web-app base URL without a trailing slash. """ override = os.getenv("IONWORKS_APP_URL") if override: return override.rstrip("/") return _derive_app_base_url(getattr(self._client, "api_url", None)) def _project_path(self, suffix: str, project_id: str | None) -> str: """Build an absolute app URL nested under a project. Resolves ``project_id`` (explicit argument, else client default) and prepends the shared ``/dashboard/projects/{project_id}`` root that every resource route lives under. Parameters ---------- suffix : str Path under the project root, beginning with ``/`` (e.g. ``/models/abc``). project_id : str | None Explicit project ID, or None to use the client default. Returns ------- str The absolute URL on the client's web-app host. """ project_id = resolve_project_id(self._client, project_id) return f"{self._app_base_url}/dashboard/projects/{project_id}{suffix}"
[docs] def project(self, project_id: str | None = None) -> str: """Build a link to a project's landing page. The web app has no bare project route; opening a project lands on its studies page (``/dashboard/projects/{p}/studies``), so this returns that URL. Parameters ---------- project_id : str | None, optional The project ID. Defaults to the project_id set on the Ionworks client (resolved from the ``IONWORKS_PROJECT_ID`` env var if not passed to the client). Returns ------- str URL of the project's studies landing page. """ return self._project_path("/studies", project_id)
[docs] def model(self, model_id: str, project_id: str | None = None) -> str: """Build a link to a model detail page. Parameters ---------- model_id : str The model ID. project_id : str | None, optional The project ID the model belongs to. Defaults to the client's project_id. Returns ------- str URL of the model detail page. """ return self._project_path(f"/models/{model_id}", project_id)
[docs] def parameterized_model( self, parameterized_model_id: str, project_id: str | None = None, ) -> str: """Build a link to a parameterized model detail page. Parameters ---------- parameterized_model_id : str The parameterized model ID. project_id : str | None, optional The project ID the parameterized model belongs to. Defaults to the client's project_id. Returns ------- str URL of the parameterized model detail page. """ return self._project_path( f"/parameterized-models/{parameterized_model_id}", project_id )
[docs] def optimization( self, optimization_id: str, project_id: str | None = None, ) -> str: """Build a link to an optimization detail page. Parameters ---------- optimization_id : str The optimization ID. project_id : str | None, optional The project ID the optimization belongs to. Defaults to the client's project_id. Returns ------- str URL of the optimization detail page. """ return self._project_path(f"/optimizations/{optimization_id}", project_id)
[docs] def pipeline(self, pipeline_id: str, project_id: str | None = None) -> str: """Build a link to a pipeline detail page. Parameters ---------- pipeline_id : str The pipeline ID. project_id : str | None, optional The project ID the pipeline belongs to. Defaults to the client's project_id. Returns ------- str URL of the pipeline detail page. """ return self._project_path(f"/pipelines/{pipeline_id}", project_id)
[docs] def study(self, study_id: str, project_id: str | None = None) -> str: """Build a link to a study detail page. Parameters ---------- study_id : str The study ID. project_id : str | None, optional The project ID the study belongs to. Defaults to the client's project_id. Returns ------- str URL of the study detail page. """ return self._project_path(f"/studies/{study_id}", project_id)
[docs] def simulation( self, simulation_id: str, parameterized_model_id: str | None = None, project_id: str | None = None, ) -> str: """Build a link to a simulation detail page (parameterized-model view). A simulation's natural owner is its parameterized model — the simulation row carries a ``parameterized_model_id`` (one per simulation), so this method builds the parameterized-model-nested route. When ``parameterized_model_id`` is not supplied, it is fetched from the simulation via ``client.simulation.get(simulation_id)``. Parameters ---------- simulation_id : str The simulation ID. parameterized_model_id : str | None, optional The parameterized model the simulation belongs to. When omitted, it is derived by fetching the simulation (one network call). project_id : str | None, optional The project ID the simulation belongs to. Defaults to the client's project_id. Returns ------- str URL of the simulation detail page nested under its parameterized model. Raises ------ ValueError If ``parameterized_model_id`` is not supplied and the fetched simulation has no ``parameterized_model_id`` (e.g. a study-only simulation). Pass ``parameterized_model_id`` explicitly in that case. """ if parameterized_model_id is None: simulation = self._client.simulation.get(simulation_id) parameterized_model_id = simulation.get("parameterized_model_id") if not parameterized_model_id: raise ValueError( f"Simulation {simulation_id} has no parameterized_model_id; " "pass parameterized_model_id explicitly." ) return self._project_path( f"/parameterized-models/{parameterized_model_id}" f"/simulations/{simulation_id}", project_id, )
[docs] def protocol(self, protocol_id: str, project_id: str | None = None) -> str: """Build a link to a protocol detail page. Parameters ---------- protocol_id : str The protocol (template) ID. project_id : str | None, optional The project ID the protocol belongs to. Defaults to the client's project_id. Returns ------- str URL of the protocol detail page. """ return self._project_path(f"/protocols/{protocol_id}", project_id)
[docs] def material(self, material_id: str, project_id: str | None = None) -> str: """Build a link to a material detail page. Parameters ---------- material_id : str The material ID. project_id : str | None, optional The project ID the material belongs to. Defaults to the client's project_id. Returns ------- str URL of the material detail page. """ return self._project_path(f"/materials/{material_id}", project_id)
[docs] def cell_specs(self, project_id: str | None = None) -> str: """Build a link to the cell specifications list page. Cell specifications have no standalone detail route in the web app; this links to their list page within the project's data section. Parameters ---------- project_id : str | None, optional The project ID. Defaults to the client's project_id. Returns ------- str URL of the cell specifications list page. """ return self._project_path("/data/specs", project_id)
[docs] def cell_instances( self, spec_id: str, project_id: str | None = None, ) -> str: """Build a link to the cell instances list page for a specification. Cell instances have no standalone detail route in the web app; this links to the instances list nested under their cell specification. Parameters ---------- spec_id : str The cell specification ID the instances belong to. project_id : str | None, optional The project ID. Defaults to the client's project_id. Returns ------- str URL of the cell instances list page. """ return self._project_path(f"/data/specs/{spec_id}/instances", project_id)
[docs] def measurement( self, measurement_id: str, project_id: str | None = None, ) -> str: """Build a link to a cell measurement detail page. Parameters ---------- measurement_id : str The cell measurement ID. project_id : str | None, optional The project ID the measurement belongs to. Defaults to the project_id set on the Ionworks client (resolved from the ``IONWORKS_PROJECT_ID`` env var if not passed to the client). Returns ------- str URL of the measurement detail page on ``https://app.ionworks.com``. """ return self._project_path(f"/data/measurements/{measurement_id}", project_id)