"""Проекты-метки: таблица project, проверка метки, создание и список.""" import unicodedata from . import db MAX_LEN = 100 _FORBIDDEN = ',"\'()\\' _FIELDS = "label, name, board_id, board_type, created_at" # долг hub-modules: проекты → модуль jira (change wiki) class ProjectError(ValueError): """Ошибка данных проекта; field — label | name | board_id.""" def __init__(self, field: str, reason: str): super().__init__(f"{field}: {reason}") self.field = field class ProjectExists(ProjectError): def __init__(self, label: str): super().__init__("label", f"проект с такой меткой уже есть: {label}") def label_error(label) -> str | None: if not isinstance(label, str) or not label: return "ожидается непустая строка" if len(label) > MAX_LEN: return f"длиннее {MAX_LEN} символов" if any(c.isspace() or c in _FORBIDDEN or unicodedata.category(c) == "Cc" for c in label): return "пробелы, управляющие символы и символы , \" ' ( ) \\ недопустимы" return None def validate(label, name, board_id) -> tuple[str, str, int | None]: """Проверить поля проекта; вернуть (метка, название без пробелов по краям, доска).""" error = label_error(label) if error: raise ProjectError("label", error) if not isinstance(name, str) or not name.strip(): raise ProjectError("name", "ожидается непустая строка") if len(name.strip()) > MAX_LEN: raise ProjectError("name", f"длиннее {MAX_LEN} символов") # долг hub-modules: проекты → модуль jira (change wiki) if board_id is not None and (not isinstance(board_id, int) or isinstance(board_id, bool) or board_id <= 0): raise ProjectError("board_id", "ожидается целое > 0 или null") return label, name.strip(), board_id def get(conn, label: str) -> dict | None: row = conn.execute(f"SELECT {_FIELDS} FROM project WHERE label = ?", (label,)).fetchone() return None if row is None else dict(row) def create(conn, label: str, name: str, board_id: int | None = None) -> dict: label, name, board_id = validate(label, name, board_id) with db.transaction(conn): if label.casefold() in {x.casefold() for x in labels(conn)}: raise ProjectExists(label) # долг hub-modules: проекты → модуль jira (change wiki) conn.execute("INSERT INTO project(label, name, board_id, board_type, created_at) VALUES (?, ?, ?, NULL, ?)", (label, name, board_id, db.now())) return get(conn, label) def list_projects(conn) -> list[dict]: # долг hub-modules: проекты → модуль jira (change wiki) rows = conn.execute( f"SELECT {_FIELDS}, (SELECT COUNT(*) FROM issue i WHERE EXISTS " "(SELECT 1 FROM json_each(i.labels) WHERE value = project.label)) AS issues_count " "FROM project ORDER BY created_at, label").fetchall() return [dict(r) for r in rows] def light_list(conn) -> list[dict]: """Проекты без issues_count (label, board_id, board_type), порядок как у list_projects.""" # долг hub-modules: проекты → модуль jira (change wiki) return [dict(r) for r in conn.execute("SELECT label, board_id, board_type FROM project ORDER BY created_at, label")] def labels(conn) -> list[str]: return [r["label"] for r in conn.execute("SELECT label FROM project ORDER BY created_at, label")]