import re from typing import Any, Final, Mapping from unicodedata import normalize from pydantic.dataclasses import dataclass from mdrsclient.models.utils import iso8601_to_user_friendly class FolderAccessLevel: # Bit Mask # - bit 0: Is Private # - bit 1: Is Public # - bit 2: With Password # - bit 3-7: (Reserved) # - bit 8-15: Restricted Open, one bit per restrict network slot (slot 1 is bit 8) # - bit 16-: Project Open, one bit per project bit_position (bit_position 0 is bit 16) STORAGE: Final[int] = 0x0000 PRIVATE: Final[int] = 0x0001 PUBLIC: Final[int] = 0x0002 PASSWORD: Final[int] = 0x0004 RESERVED: Final[int] = 0x00F8 RESTRICT_SHIFT: Final[int] = 8 RESTRICT_SLOTS: Final[int] = 8 PROJECT_SHIFT: Final[int] = 16 # The server keeps no names for its restrict networks, so the one in use is named here. # Any other slot is named after its number. RESTRICT_NAMES: Final[dict[int, str]] = {1: "CBS"} LEVEL_NAMES: Final[dict[int, str]] = {STORAGE: "Storage", PRIVATE: "Private", PUBLIC: "Public"} @staticmethod def restrict_name(slot: int) -> str: return FolderAccessLevel.RESTRICT_NAMES.get(slot, f"Restrict{slot}") @staticmethod def project_name(bit_position: int, projects: Mapping[int, str] | None = None) -> str: name = (projects or {}).get(bit_position) return name if name else f"Project{bit_position}" @staticmethod def id2label(id: int, projects: Mapping[int, str] | None = None) -> str | None: """ Label an access level from its bits, e.g. 0x0104 is "CBS or PW Open". `projects` names the projects by bit_position, as the server tells their members; any other project gets a name made from its bit_position. None for a level that has no meaning: Private or Public with any other bit, or a reserved bit. """ if id in FolderAccessLevel.LEVEL_NAMES: return FolderAccessLevel.LEVEL_NAMES[id] if id < 0 or id & (FolderAccessLevel.PRIVATE | FolderAccessLevel.PUBLIC | FolderAccessLevel.RESERVED): return None names = [ FolderAccessLevel.restrict_name(slot) for slot in range(1, FolderAccessLevel.RESTRICT_SLOTS + 1) if id & (1 << (FolderAccessLevel.RESTRICT_SHIFT + slot - 1)) ] project_bits = id >> FolderAccessLevel.PROJECT_SHIFT names += [ FolderAccessLevel.project_name(bit, projects) for bit in range(project_bits.bit_length()) if project_bits & (1 << bit) ] if id & FolderAccessLevel.PASSWORD: names.append("PW") return " or ".join(names) + " Open" @staticmethod def key2id(key: str) -> int | None: """ The access level a key names: `private`, `public`, `storage`, or the parts of an open level joined by `_or_` and ending in `_open`, e.g. `cbs_or_pw_open`. A part is `pw`, `cbs`, `restrict` or `project`. """ fixed = {label.lower(): id for id, label in FolderAccessLevel.LEVEL_NAMES.items()} if key in fixed: return fixed[key] if not key.endswith("_open"): return None id = 0 for part in key.removesuffix("_open").split("_or_"): bit = FolderAccessLevel._part2bit(part) if bit is None: return None id |= bit return id @staticmethod def _part2bit(part: str) -> int | None: if part == "pw": return FolderAccessLevel.PASSWORD slots = {name.lower(): slot for slot, name in FolderAccessLevel.RESTRICT_NAMES.items()} match = re.fullmatch(r"restrict([1-9][0-9]*)", part) slot = slots.get(part, int(match.group(1)) if match else None) if slot is not None: if slot > FolderAccessLevel.RESTRICT_SLOTS: return None return 1 << (FolderAccessLevel.RESTRICT_SHIFT + slot - 1) match = re.fullmatch(r"project(0|[1-9][0-9]*)", part) if match: return 1 << (FolderAccessLevel.PROJECT_SHIFT + int(match.group(1))) return None @dataclass(frozen=True) class FolderSimple: id: str pid: str | None name: str access_level: int lock: bool size: int laboratory_id: int description: str created_at: str updated_at: str restrict_opened_at: str | None def access_level_label(self, projects: Mapping[int, str] | None = None) -> str: """The access level as shown to a user, naming the projects given by bit_position.""" label = FolderAccessLevel.id2label(self.access_level, projects) return label if label is not None else f"Unknown (0x{self.access_level:x})" @property def access_level_name(self) -> str: return self.access_level_label() @property def lock_name(self) -> str: return "locked" if self.lock else "unlocked" @property def created_at_name(self) -> str: return iso8601_to_user_friendly(self.created_at) @property def updated_at_name(self) -> str: return iso8601_to_user_friendly(self.updated_at) @dataclass(frozen=True) class Folder(FolderSimple): metadata: list[dict[str, Any]] sub_folders: list[FolderSimple] path: str def find_sub_folder(self, name: str) -> FolderSimple | None: _name = normalize("NFC", name).lower() return next((x for x in self.sub_folders if x.name.lower() == _name), None)