Source code for lucidlink.workspace

"""
Workspace context after successful authentication.
"""

import threading
import warnings
from typing import Dict, List, Optional

from .exceptions import FilespaceAlreadyLinkedError
from .filespace import Filespace
from .filespace_models import FilespaceInfo, SyncMode


[docs] class Workspace: """ Workspace context after successful authentication. Provides access to filespace operations within the authenticated workspace. Returned by ``Daemon.authenticate()``. Multiple filespaces can be linked concurrently — each ``link_filespace()`` call returns an independent ``Filespace`` that stays usable until it is unlinked. Example: .. code-block:: python credentials = ServiceAccountCredentials(token) client.login(credentials) workspace = client.get_workspace(client.list_workspaces()[0].id) print(workspace.id, workspace.name) """ def __init__(self, native_daemon, workspace_id: str, workspace_name: str): """ Initialize workspace context. Args: native_daemon: Native runtime wrapper (internal use) workspace_id: Workspace ID workspace_name: Workspace name Note: This constructor is called internally by ``Client.login()``. Users should not construct ``Workspace`` objects directly. """ self._native_daemon = native_daemon self._id = workspace_id self._name = workspace_name self._linked_filespaces: Dict[str, Filespace] = {} # filespace id -> Filespace self._aliases: Dict[str, str] = {} # name-or-id used at link time -> filespace id # Single lock for every read or write of `_linked_filespaces` / # `_aliases` — link/unlink/stop/iteration. The GIL atomises single # bytecodes but not multi-step patterns (the comprehension reassignment # in `_on_filespace_unlinked`, the find-then-link check in # `link_filespace`, dict iteration while another thread mutates), so # those need explicit serialisation. RLock because `stop()` holds the # lock while calling `filespace.unlink()`, which re-enters # `_on_filespace_unlinked` on the same thread. self._link_lock = threading.RLock() @property def id(self) -> str: """Get the workspace ID.""" return self._id @property def name(self) -> str: """Get the workspace name.""" return self._name @property def linked_filespaces(self) -> List[Filespace]: """Get the currently linked ``Filespace`` objects.""" with self._link_lock: return [fs for fs in self._linked_filespaces.values() if fs.is_linked]
[docs] def list_filespaces(self) -> List[FilespaceInfo]: """ List all filespaces in this workspace. Returns: List of FilespaceInfo objects with id, name, and created timestamp. Raises: ConnectionError: If LucidLink services are unreachable AuthenticationError: If access token expired RuntimeError: If the client is not running Example: .. code-block:: python filespaces = workspace.list_filespaces() for fs in filespaces: print(f"{fs.name}") """ return [ FilespaceInfo(id=d["id"], name=d["name"], created=d["created"]) for d in self._native_daemon.list_filespaces(self.id) ]
def _find_linked(self, identifier: str) -> Optional[Filespace]: """Resolve an identifier (link-time name or filespace id) to a live link. Caller must hold ``_link_lock``. """ fs_id = self._aliases.get(identifier) if fs_id is None and identifier in self._linked_filespaces: fs_id = identifier if fs_id is None: return None filespace = self._linked_filespaces.get(fs_id) if filespace is not None and filespace.is_linked: return filespace return None def _recover_alias_miss( self, identifier: str, error: FilespaceAlreadyLinkedError ) -> Optional[Filespace]: """Map an already-linked error back to the live Filespace and register the new alias. Returns None (caller re-raises) if the daemon's id is absent or no longer tracked — a state we can't safely recover from. Caller must hold ``_link_lock``. """ fs_id = error.filespace_id if not fs_id: return None filespace = self._linked_filespaces.get(fs_id) if filespace is None or not filespace.is_linked: return None self._aliases[identifier] = fs_id return filespace def _on_filespace_unlinked(self, filespace_id: str) -> None: """Callback from Filespace.unlink — drop the entry and any aliases that point at this id so a long-running service doesn't accumulate zombies.""" with self._link_lock: self._linked_filespaces.pop(filespace_id, None) self._aliases = {k: v for k, v in self._aliases.items() if v != filespace_id}
[docs] def stop(self) -> None: """Stop workspace — unlinks all linked filespaces. If a filespace's ``sync_mode`` is ``SYNC_ALL``, ``sync_all()`` is called before it is unlinked. Per-filespace unlink errors are swallowed so one failing link never blocks the others. Safe to call multiple times. """ # `filespace.unlink()` re-enters `_on_filespace_unlinked` on the same # thread (the lock is an RLock so the re-entry is OK). Holding the lock # across the iteration also stops a concurrent `link_filespace` from # publishing a new entry that would survive the clear below. with self._link_lock: for filespace in list(self._linked_filespaces.values()): try: filespace.unlink() except Exception: pass self._linked_filespaces.clear() self._aliases.clear()
def __repr__(self) -> str: return f"Workspace(id='{self.id}', name='{self.name}')"