|
| 1 | +"""Fail fast when a workspace-scoped request has no workspace to go to. |
| 2 | +
|
| 3 | +Most operations are scoped to a workspace through the ``X-Workspace-Id`` |
| 4 | +header (the ``WorkspaceId`` security scheme). The generated client simply |
| 5 | +skips a scheme that has no configured value, so a ``Configuration`` built |
| 6 | +without ``workspace_id`` sends the request anyway and the server answers with |
| 7 | +a bare 404 that says nothing about what was missing. |
| 8 | +
|
| 9 | +:func:`ensure_workspace` runs before the request is sent (wired into |
| 10 | +``ApiClient.update_params_for_auth`` by ``scripts/patch_workspace_guard.py``) |
| 11 | +and raises :class:`MissingWorkspaceError` naming the constructor argument and |
| 12 | +the environment variable. Operations whose security does not list |
| 13 | +``WorkspaceId`` (workspace management itself) are unaffected, as are calls |
| 14 | +that supply the header explicitly or override auth per request. |
| 15 | +""" |
| 16 | + |
| 17 | +from __future__ import annotations |
| 18 | + |
| 19 | +from typing import Any, Iterable, Mapping, Optional |
| 20 | + |
| 21 | +from hotdata.exceptions import ApiValueError |
| 22 | + |
| 23 | +WORKSPACE_SCHEME = "WorkspaceId" |
| 24 | +WORKSPACE_HEADER = "X-Workspace-Id" |
| 25 | + |
| 26 | + |
| 27 | +class MissingWorkspaceError(ApiValueError): |
| 28 | + """Raised before sending a workspace-scoped request when no workspace id |
| 29 | + is configured. |
| 30 | +
|
| 31 | + Pass ``workspace_id=`` to :class:`hotdata.Configuration`, set the |
| 32 | + ``HOTDATA_WORKSPACE`` environment variable, or send the ``X-Workspace-Id`` |
| 33 | + header explicitly for the call. |
| 34 | + """ |
| 35 | + |
| 36 | + def __init__(self, resource_path: str, method: str) -> None: |
| 37 | + self.resource_path = resource_path |
| 38 | + self.method = method |
| 39 | + super().__init__( |
| 40 | + f"{method} {resource_path} is scoped to a workspace but no workspace id " |
| 41 | + "is configured. Pass workspace_id=... to hotdata.Configuration(...), " |
| 42 | + "set the HOTDATA_WORKSPACE environment variable, or send the " |
| 43 | + f"{WORKSPACE_HEADER} header explicitly (_headers=...)." |
| 44 | + ) |
| 45 | + |
| 46 | + |
| 47 | +def ensure_workspace( |
| 48 | + configuration: Any, |
| 49 | + auth_settings: Optional[Iterable[str]], |
| 50 | + headers: Optional[Mapping[str, Any]], |
| 51 | + resource_path: str, |
| 52 | + method: str, |
| 53 | + request_auth: Optional[Mapping[str, Any]] = None, |
| 54 | +) -> None: |
| 55 | + """Raise :class:`MissingWorkspaceError` if the operation needs a workspace |
| 56 | + id and none would be sent. |
| 57 | +
|
| 58 | + :param configuration: The client's :class:`hotdata.Configuration`. |
| 59 | + :param auth_settings: The operation's security scheme names. |
| 60 | + :param headers: Header params already assembled for the request. |
| 61 | + :param request_auth: A per-request auth override; when given the |
| 62 | + configuration is bypassed entirely, so the check is skipped. |
| 63 | + """ |
| 64 | + if request_auth: |
| 65 | + return |
| 66 | + if not auth_settings or WORKSPACE_SCHEME not in auth_settings: |
| 67 | + return |
| 68 | + if headers and any( |
| 69 | + str(name).lower() == WORKSPACE_HEADER.lower() for name in headers |
| 70 | + ): |
| 71 | + return |
| 72 | + settings = configuration.auth_settings() |
| 73 | + scheme = settings.get(WORKSPACE_SCHEME) if settings else None |
| 74 | + if scheme and scheme.get("value"): |
| 75 | + return |
| 76 | + raise MissingWorkspaceError(resource_path, method) |
| 77 | + |
| 78 | + |
| 79 | +__all__ = [ |
| 80 | + "WORKSPACE_HEADER", |
| 81 | + "WORKSPACE_SCHEME", |
| 82 | + "MissingWorkspaceError", |
| 83 | + "ensure_workspace", |
| 84 | +] |
0 commit comments