Wiki/docmost/setup/seed.py
Corentin JOGUET d5558caf9a
Some checks are pending
CI / Lint bridge (Biome) (push) Waiting to run
CI / Type-check bridge (push) Blocked by required conditions
CI / Tests unit bridge (push) Blocked by required conditions
CI / Tests integration bridge (push) Blocked by required conditions
CI / Security scan (push) Waiting to run
CI / Docker build + healthcheck (push) Blocked by required conditions
fix(docmost-seed): handle Docmost data envelope + add format field
Docmost API responses wrap : {data, success, status} (3 keys top-level).
- _post auto-unwrap si payload contient `data` dict|list
- create_page : ajout du field 'format' (markdown|json|html) requis
- welcome_page_content() : retourne markdown plain (Docmost convertit auto en Tiptap)

Iteration 3 BUILD validee en local :
  Workspace : Acadenice (id 019e034d-...)
  3 spaces  : CFA, Agence, Interne
  Page test : Welcome formation-hub dans CFA
  Share URL : http://localhost:3000/share/019e0352-7fc4-7639...

Endpoints reverse-engineered confirmes fonctionnels :
  /api/auth/setup, /api/auth/login, /api/workspace/info,
  /api/spaces/, /api/spaces/create, /api/pages/create, /api/shares/create
2026-05-07 18:43:39 +02:00

233 lines
9 KiB
Python

#!/usr/bin/env python3
"""Docmost seed script — utilise les endpoints internes (non-officiels mais AGPL-legal).
Cree workspace + admin + 3 spaces + page test + share link via reverse-engineering
des controllers Docmost (cf github.com/docmost/docmost/apps/server/src/core).
ATTENTION : ces endpoints ne sont pas publiquement documentes par Docmost.
Ils peuvent changer entre versions. Testez le script apres chaque bump Docmost.
Usage :
DOCMOST_URL=http://localhost:3000 \\
DOCMOST_ADMIN_EMAIL=admin@acadenice.fr \\
DOCMOST_ADMIN_PASSWORD=... \\
DOCMOST_ADMIN_NAME='Corentin JOGUET' \\
DOCMOST_WORKSPACE_NAME=Acadenice \\
python docmost/setup/seed.py [--schema docmost/setup/spaces.yaml]
"""
import argparse
import json
import os
import sys
from pathlib import Path
from typing import Any
import requests
import yaml
class DocmostSeed:
def __init__(self, base_url: str) -> None:
self.base_url = base_url.rstrip("/")
self.session = requests.Session()
self.session.headers.update({"Content-Type": "application/json"})
self.workspace_id: str | None = None
def _post(self, path: str, body: dict[str, Any] | None = None) -> dict[str, Any]:
"""POST + auto-unwrap `data` envelope (Docmost API wrap convention)."""
url = f"{self.base_url}{path}"
r = self.session.post(url, json=body or {})
if r.status_code >= 300:
print(f" [ERROR] POST {path}{r.status_code}\n body={body}\n resp={r.text[:500]}")
r.raise_for_status()
if not r.text:
return {}
payload = r.json()
# Docmost wraps responses : {data: ..., success: bool, status: int}
if isinstance(payload, dict) and "data" in payload and isinstance(payload["data"], (dict, list)):
return payload["data"]
return payload
def setup_admin_and_workspace(self, name: str, email: str, password: str, workspace_name: str) -> bool:
"""POST /api/auth/setup — bootstrap workspace + admin (1ere fois seulement).
Retourne True si setup execute, False si workspace deja initialise.
"""
try:
self._post(
"/api/auth/setup",
{"name": name, "email": email, "password": password, "workspaceName": workspace_name},
)
print(f" [setup] Workspace '{workspace_name}' + admin '{email}' crees")
return True
except requests.HTTPError as e:
if e.response is not None and e.response.status_code in (400, 403, 404):
print(" [setup] Workspace deja initialise — skip")
return False
raise
def login(self, email: str, password: str) -> None:
self._post("/api/auth/login", {"email": email, "password": password})
print(f" [auth] Logged in as {email}")
def get_workspace_info(self) -> dict[str, Any]:
info = self._post("/api/workspace/info")
self.workspace_id = info.get("id") or info.get("workspaceId")
print(f" [workspace] id={self.workspace_id}")
return info
def list_spaces(self) -> list[dict[str, Any]]:
try:
data = self._post("/api/spaces/", {"page": 1, "limit": 100})
# Apres unwrap de data, on a { items: [...], meta: {...} }
if isinstance(data, dict):
return data.get("items") or []
if isinstance(data, list):
return data
return []
except requests.HTTPError:
return []
def create_space(self, name: str, description: str, slug: str, visibility: str = "open") -> dict[str, Any]:
body = {
"name": name,
"description": description,
"slug": slug,
"visibility": visibility,
}
return self._post("/api/spaces/create", body)
def get_or_create_space(self, name: str, description: str, slug: str, visibility: str = "open") -> str:
existing = self.list_spaces()
for s in existing:
if s.get("name") == name or s.get("slug") == slug:
sid = s.get("id")
print(f" [space] Reuse '{name}' id={sid}")
return sid
created = self.create_space(name, description, slug, visibility)
sid = created.get("id")
print(f" [space] Created '{name}' id={sid}")
return sid
def create_page(self, space_id: str, title: str, content: str | dict[str, Any] | None = None, fmt: str = "markdown") -> str:
body: dict[str, Any] = {"spaceId": space_id, "title": title, "format": fmt}
if content is not None:
body["content"] = content
result = self._post("/api/pages/create", body)
pid = result.get("id")
print(f" [page] Created '{title}' id={pid} in space={space_id}")
return pid
def create_share(self, page_id: str, expires_at: str | None = None) -> dict[str, Any]:
body: dict[str, Any] = {"pageId": page_id, "includeSubPages": False}
if expires_at:
body["expiresAt"] = expires_at
result = self._post("/api/shares/create", body)
share_id = result.get("id") or result.get("key")
share_url = result.get("url") or f"{self.base_url}/share/{share_id}"
print(f" [share] Created share for page={page_id} id={share_id}")
print(f" URL: {share_url}")
return result
def welcome_page_content() -> str:
"""Page Welcome en markdown — Docmost le converti en Tiptap auto."""
return """# Welcome formation-hub
Wiki Docmost interne Acadenice — voir `docs/` du repo wiki pour la conception complete.
## Stack
- **Docmost** : ce wiki (AGPL self-host)
- **Baserow** : DBs structurees (MIT self-host) — http://localhost:8080
- **Bridge service** : Phase 2
## Diagrammes natifs
Ajouter via le slash menu :
- `/mermaid` pour les diagrammes Mermaid
- `/drawio` pour les diagrammes Draw.io
- `/excalidraw` pour les whiteboards Excalidraw
## Liens utiles
- Repo Forgejo : https://git.acadenice.com/AcadeNice/Wiki
- Stack locale : Docmost (3000) + Baserow (8080)
- Documentation projet : `docs/00-readme.md` du repo
"""
def main() -> int:
parser = argparse.ArgumentParser(description="Seed Docmost formation-hub")
parser.add_argument("--schema", default=str(Path(__file__).parent / "spaces.yaml"))
parser.add_argument("--skip-setup", action="store_true", help="Skip workspace bootstrap (deja fait)")
args = parser.parse_args()
base_url = os.environ.get("DOCMOST_URL", "http://localhost:3000")
email = os.environ.get("DOCMOST_ADMIN_EMAIL")
password = os.environ.get("DOCMOST_ADMIN_PASSWORD")
name = os.environ.get("DOCMOST_ADMIN_NAME", "Admin")
workspace_name = os.environ.get("DOCMOST_WORKSPACE_NAME", "Acadenice")
if not email or not password:
print("ERROR: set DOCMOST_ADMIN_EMAIL and DOCMOST_ADMIN_PASSWORD env vars", file=sys.stderr)
return 1
schema = yaml.safe_load(Path(args.schema).read_text())
seeder = DocmostSeed(base_url)
try:
if not args.skip_setup:
print(f"\n[1/5] Bootstrap workspace {workspace_name}")
seeder.setup_admin_and_workspace(name, email, password, workspace_name)
print(f"\n[2/5] Login")
seeder.login(email, password)
print(f"\n[3/5] Workspace info")
try:
seeder.get_workspace_info()
except requests.HTTPError:
print(" [workspace] info endpoint a echoue — continue")
print(f"\n[4/5] Spaces ({len(schema.get('spaces', []))})")
space_ids: dict[str, str] = {}
for sp in schema.get("spaces", []):
sid = seeder.get_or_create_space(
name=sp["name"],
description=sp.get("description", "").strip(),
slug=sp.get("slug", sp["name"].lower()),
visibility=("open" if sp.get("visibility") == "workspace_members" else "private"),
)
if sid:
space_ids[sp["name"]] = sid
print(f"\n[5/5] Welcome page + share link")
welcome = schema.get("welcome_page") or {}
target_space = welcome.get("space", "CFA")
if target_space in space_ids:
page_id = seeder.create_page(
space_id=space_ids[target_space],
title=welcome.get("title", "Welcome formation-hub"),
content=welcome_page_content(),
)
if page_id and welcome.get("share_link", {}).get("enabled"):
seeder.create_share(page_id=page_id)
else:
print(f" [skip] Space '{target_space}' introuvable, page Welcome non creee")
print("\n=== Seed Docmost OK ===")
print(f" Workspace : {workspace_name}")
print(f" Spaces : {len(space_ids)}")
print(f" Welcome : page + share link dans space '{target_space}'")
except requests.HTTPError as e:
print(f"\nHTTP error: {e}", file=sys.stderr)
if e.response is not None:
print(f"Response: {e.response.text[:500]}", file=sys.stderr)
return 2
return 0
if __name__ == "__main__":
sys.exit(main())