Zum Inhalt

API Reference

DownloadOZG

Source code in src/form_download_helper/__init__.py
class DownloadOZG:
    def __init__(self, path, uri, username, password, version, package=None):
        self.path = path
        self.uri = uri
        self.auth = HTTPBasicAuth(username, password)
        self.version = version
        self.package = package or derive_package_name(Path(path))

    def get(self):
        rich.print(
            f"[bold yellow]INFO![/bold yellow] [green]Download - {self.uri}[/green] :boom:"
        )
        jsonschema = requests.get(
            f"{self.uri}/@@meta-schema?version={self.version}", auth=self.auth
        ).text
        ui_schema = requests.get(
            f"{self.uri}/@@simple-ui-schema-view?version={self.version}", auth=self.auth
        ).text
        js_schema = json.loads(jsonschema)
        form_name = resolve_form_name(js_schema, self.uri)
        formatted = format_version(self.version)
        schema_dir_path = Path(f"{self.path}/{form_name}")
        form_dir_path = Path(f"{self.path}/{form_name}/{formatted}")
        form_dir_path.mkdir(parents=True, exist_ok=True)
        is_wizard = js_schema.get("type") == "wizard"

        with open(form_dir_path / "schema.json", "w") as f:
            rich.print(f"[green]Write - {form_dir_path}/schema.json [/green]")
            f.write(jsonschema)
        with open(form_dir_path / "ui_schema.json", "w") as f:
            rich.print(f"[green]Write - {form_dir_path}/ui_schema.json [/green]")
            f.write(ui_schema)
        with open(form_dir_path / "template.pt", "w") as f:
            rich.print(f"[green]Write - {form_dir_path}/template.pt [/green]")
            if is_wizard:
                f.write(WIZARD_TEMPLATE_PT)
            else:
                deform = requests.get(
                    f"{self.uri}/@@cpt-generator?version={self.version}", auth=self.auth
                ).text
                f.write(deform)

        # For wizard forms, also generate the wizard-mode template
        if is_wizard:
            with open(form_dir_path / "wizard_template.pt", "w") as f:
                rich.print(f"[green]Write - {form_dir_path}/wizard_template.pt [/green]")
                f.write(WIZARD_MODE_TEMPLATE_PT)

        # Write form.py only if it doesn't exist (preserve user customizations)
        form_py_path = form_dir_path.parent / "form.py"
        if form_py_path.exists():
            rich.print(f"[bold yellow]Skip - {form_py_path} (already exists)[/bold yellow]")
        else:
            with open(form_py_path, "w") as f:
                rich.print(f"[green]Write - {form_py_path} [/green]")
                if is_wizard:
                    rich.print(f"[bold cyan]Using WIZARD_TEMPLATE for {form_name}[/bold cyan]")
                    f.write(WIZARD_TEMPLATE.format(
                        doc_type=form_name,
                        version=self.version,
                        package=self.package
                    ))
                else:
                    f.write(FORM_TEMPLATE.format(
                        doc_type=form_name,
                        version=self.version
                    ))

        with open(form_dir_path / "__init__.py", "w") as f:
            rich.print(f"[green]Write - {form_dir_path}/__init__.py [/green]")
            f.write("# Package")
        with open(schema_dir_path / "__init__.py", "w") as f:
            rich.print(f"[green]Write - {schema_dir_path}/__init__.py [/green]")
            f.write("# Package")
        # TESTS
        form_test_dir_path = form_dir_path / "tests"
        form_dir_path.mkdir(parents=True, exist_ok=True)
        form_test_dir_path.mkdir(exist_ok=True)
        # Add __init__.py to tests directory to make it a proper package
        # This prevents pytest import conflicts when collecting tests
        with open(form_test_dir_path / "__init__.py", "w") as f:
            f.write("# Package")
        # Use unique test file name based on form name to avoid import conflicts
        test_file_name = f"test_{form_name.lower()}.py"
        with open(form_test_dir_path / test_file_name, "w") as f:
            rich.print(f"[green]Write - {form_test_dir_path}/{test_file_name} [/green]")
            f.write(FORM_TEST_TEMPLATE.format(doc_type=form_name))
        with open(form_test_dir_path / "test_acceptance.py", "w") as f:
            rich.print(f"[green]Write - {form_test_dir_path}/test_acceptance.py [/green]")
            f.write(ACCEPTANCE_TEST_TEMPLATE.format(doc_type=form_name))

        # Check if this is a wizard and download steps
        if is_wizard:
            rich.print("[bold cyan]Detected wizard type, downloading steps...[/bold cyan]")
            self.download_wizard_steps(js_schema, form_dir_path)

        rich.print("READY !!! :boom: ")

    def download_wizard_steps(self, js_schema, form_dir_path):
        """Download all wizard steps"""
        steps = list(js_schema.get("schema", {}).get("properties", {}).keys())
        rich.print(f"[cyan]Found {len(steps)} wizard steps[/cyan]")

        steps_dir = form_dir_path / "steps"
        steps_dir.mkdir(exist_ok=True)

        for step_name in steps:
            rich.print(f"[yellow]Downloading step: {step_name}[/yellow]")
            step_url = f"{self.uri}/{step_name}"

            try:
                # Download step schema
                step_jsonschema = requests.get(
                    f"{step_url}/@@meta-schema?version={self.version}", auth=self.auth
                ).text
                step_ui_schema = requests.get(
                    f"{step_url}/@@simple-ui-schema-view?version={self.version}", auth=self.auth
                ).text
                step_deform = requests.get(
                    f"{step_url}/@@cpt-generator?version={self.version}", auth=self.auth
                ).text

                # Create step directory
                step_dir = steps_dir / step_name
                step_dir.mkdir(exist_ok=True)

                # Write step files
                with open(step_dir / "schema.json", "w") as f:
                    rich.print(f"[green]  Write - {step_dir}/schema.json [/green]")
                    f.write(step_jsonschema)
                with open(step_dir / "ui_schema.json", "w") as f:
                    rich.print(f"[green]  Write - {step_dir}/ui_schema.json [/green]")
                    f.write(step_ui_schema)
                if "error_type" in step_deform or step_deform.strip().startswith("{"):
                    rich.print(f"[bold red]ERROR: @@cpt-generator failed for step '{step_name}': {step_deform.strip()}[/bold red]")
                    rich.print(f"[bold red]  URL: {step_url}/@@cpt-generator?version={self.version}[/bold red]")
                    rich.print(f"[yellow]  Skipping template.pt for step '{step_name}'[/yellow]")
                else:
                    # Strip csrf_token line - wizard handles csrf at form level,
                    # not in individual step templates
                    clean_deform = "\n".join(
                        line for line in step_deform.splitlines()
                        if "csrf_token" not in line
                    )
                    with open(step_dir / "template.pt", "w") as f:
                        rich.print(f"[green]  Write - {step_dir}/template.pt [/green]")
                        f.write(clean_deform)
                with open(step_dir / "__init__.py", "w") as f:
                    f.write("# Package")

            except Exception as e:
                rich.print(f"[red]Error downloading step {step_name}: {e}[/red]")

download_wizard_steps(js_schema, form_dir_path)

Download all wizard steps

Source code in src/form_download_helper/__init__.py
def download_wizard_steps(self, js_schema, form_dir_path):
    """Download all wizard steps"""
    steps = list(js_schema.get("schema", {}).get("properties", {}).keys())
    rich.print(f"[cyan]Found {len(steps)} wizard steps[/cyan]")

    steps_dir = form_dir_path / "steps"
    steps_dir.mkdir(exist_ok=True)

    for step_name in steps:
        rich.print(f"[yellow]Downloading step: {step_name}[/yellow]")
        step_url = f"{self.uri}/{step_name}"

        try:
            # Download step schema
            step_jsonschema = requests.get(
                f"{step_url}/@@meta-schema?version={self.version}", auth=self.auth
            ).text
            step_ui_schema = requests.get(
                f"{step_url}/@@simple-ui-schema-view?version={self.version}", auth=self.auth
            ).text
            step_deform = requests.get(
                f"{step_url}/@@cpt-generator?version={self.version}", auth=self.auth
            ).text

            # Create step directory
            step_dir = steps_dir / step_name
            step_dir.mkdir(exist_ok=True)

            # Write step files
            with open(step_dir / "schema.json", "w") as f:
                rich.print(f"[green]  Write - {step_dir}/schema.json [/green]")
                f.write(step_jsonschema)
            with open(step_dir / "ui_schema.json", "w") as f:
                rich.print(f"[green]  Write - {step_dir}/ui_schema.json [/green]")
                f.write(step_ui_schema)
            if "error_type" in step_deform or step_deform.strip().startswith("{"):
                rich.print(f"[bold red]ERROR: @@cpt-generator failed for step '{step_name}': {step_deform.strip()}[/bold red]")
                rich.print(f"[bold red]  URL: {step_url}/@@cpt-generator?version={self.version}[/bold red]")
                rich.print(f"[yellow]  Skipping template.pt for step '{step_name}'[/yellow]")
            else:
                # Strip csrf_token line - wizard handles csrf at form level,
                # not in individual step templates
                clean_deform = "\n".join(
                    line for line in step_deform.splitlines()
                    if "csrf_token" not in line
                )
                with open(step_dir / "template.pt", "w") as f:
                    rich.print(f"[green]  Write - {step_dir}/template.pt [/green]")
                    f.write(clean_deform)
            with open(step_dir / "__init__.py", "w") as f:
                f.write("# Package")

        except Exception as e:
            rich.print(f"[red]Error downloading step {step_name}: {e}[/red]")

derive_name_from_uri(uri)

Derive a form name from the URI slug when the server returns 'unknown_form'.

Takes the last path segment of the URI and converts hyphens to underscores to produce a valid Python-friendly directory/identifier name.

Example

https://example.de/community/guvh/anfrage-zahnarzt -> anfrage_zahnarzt

Source code in src/form_download_helper/__init__.py
def derive_name_from_uri(uri: str) -> str:
    """Derive a form name from the URI slug when the server returns 'unknown_form'.

    Takes the last path segment of the URI and converts hyphens to underscores
    to produce a valid Python-friendly directory/identifier name.

    Example:
        https://example.de/community/guvh/anfrage-zahnarzt -> anfrage_zahnarzt
    """
    # Strip trailing slashes and take the last path segment
    slug = uri.rstrip("/").rsplit("/", 1)[-1]
    return slug.replace("-", "_")

derive_package_name(path)

Derive package name from path, using project root detection.

Resolves the path to absolute, finds the project root by looking for pyproject.toml, setup.py, or setup.cfg, then derives the package name from the relative path.

Examples:

/project/src/myapp/forms -> myapp.forms (if src/ exists) /project/myapp/forms -> myapp.forms (relative to project root)

Source code in src/form_download_helper/__init__.py
def derive_package_name(path: Path) -> str:
    """Derive package name from path, using project root detection.

    Resolves the path to absolute, finds the project root by looking for
    pyproject.toml, setup.py, or setup.cfg, then derives the package name
    from the relative path.

    Examples:
        /project/src/myapp/forms -> myapp.forms (if src/ exists)
        /project/myapp/forms -> myapp.forms (relative to project root)
    """
    abs_path = path.resolve()
    project_root = find_project_root(abs_path)

    if project_root is None:
        # Fallback: use last two parts of the path
        parts = abs_path.parts
        return ".".join(parts[-2:]) if len(parts) >= 2 else parts[-1] if parts else "package"

    # Check if there's a src/ directory
    src_dir = project_root / "src"
    if src_dir.exists() and abs_path.is_relative_to(src_dir):
        rel_path = abs_path.relative_to(src_dir)
    elif abs_path.is_relative_to(project_root):
        rel_path = abs_path.relative_to(project_root)
    else:
        # Path is outside project, use last two parts
        parts = abs_path.parts
        return ".".join(parts[-2:]) if len(parts) >= 2 else parts[-1] if parts else "package"

    return ".".join(rel_path.parts)

download(path, username, password, version, uri, package=None)

Download form schemas from OZG server.

Source code in src/form_download_helper/__init__.py
def download(
    path: Path,
    username: str,
    password: str,
    version: int,
    uri: str,
    package: Optional[str] = None,
):
    """Download form schemas from OZG server."""
    ozg_sync = DownloadOZG(path, uri, username, password, version, package)
    ozg_sync.get()

ensure_valid_identifier(name)

Prefix names that start with a digit so they are valid Python identifiers.

Source code in src/form_download_helper/__init__.py
def ensure_valid_identifier(name: str) -> str:
    """Prefix names that start with a digit so they are valid Python identifiers."""
    if name and name[0].isdigit():
        return f"F_{name}"
    return name

find_project_root(start_path)

Find project root by looking for pyproject.toml, setup.py, or setup.cfg.

Source code in src/form_download_helper/__init__.py
def find_project_root(start_path: Path) -> Optional[Path]:
    """Find project root by looking for pyproject.toml, setup.py, or setup.cfg."""
    current = start_path.resolve()

    # Walk up the directory tree
    for parent in [current] + list(current.parents):
        for marker in PROJECT_ROOT_MARKERS:
            if (parent / marker).exists():
                return parent
    return None

resolve_form_name(js_schema, uri)

Return a usable form name, falling back to the URI slug if the server returned 'unknown_form'.

Source code in src/form_download_helper/__init__.py
def resolve_form_name(js_schema: dict, uri: str) -> str:
    """Return a usable form name, falling back to the URI slug if the
    server returned 'unknown_form'."""
    name = js_schema.get("name", UNKNOWN_FORM_NAME)
    if name == UNKNOWN_FORM_NAME:
        derived = derive_name_from_uri(uri)
        rich.print(
            f"[bold yellow]WARNING![/bold yellow] Server returned '{UNKNOWN_FORM_NAME}' "
            f"as form name. Deriving name from URI: [cyan]{derived}[/cyan]"
        )
        return ensure_valid_identifier(derived)
    return ensure_valid_identifier(name)

Klassen

DownloadOZG

Die Hauptklasse für das Herunterladen von OZG-Formularen.

class DownloadOZG:
    def __init__(self, path, uri, username, password, version, package=None):
        """
        Initialisiert den OZG Downloader.

        Args:
            path (str): Zielverzeichnis für Downloads
            uri (str): URI des Formulars
            username (str): Benutzername für Authentication
            password (str): Passwort für Authentication
            version (int): Versionsnummer des Formulars
            package (str, optional): Python-Paketname. Wird automatisch
                aus dem Pfad abgeleitet, wenn nicht angegeben.
        """

Methoden

get()
def get(self):
    """
    Lädt alle Formular-Dateien herunter und erstellt die Verzeichnisstruktur.

    Downloads:
    - JSON Schema (@@meta-schema)
    - UI Schema (@@simple-ui-schema-view)
    - Page Template (@@cpt-generator) – nur für Nicht-Wizard-Formulare

    Generiert:
    - Python Formularklassen (form.py)
    - Test-Dateien (test_<formname>.py, test_acceptance.py)
    - Package-Struktur (__init__.py)

    Bei Wizard-Formularen werden zusätzlich die einzelnen
    Wizard-Steps heruntergeladen.
    """
download_wizard_steps()
def download_wizard_steps(self, js_schema, form_dir_path):
    """
    Lädt alle Wizard-Steps herunter.

    Für jeden Step werden schema.json, ui_schema.json und
    template.pt in ein eigenes Unterverzeichnis geschrieben.

    Args:
        js_schema (dict): Das geparste JSON-Schema des Wizards
        form_dir_path (Path): Pfad zum Versions-Verzeichnis
    """

Funktionen

download()

def download(
    path: Path,
    username: str,
    password: str,
    version: int,
    uri: str,
    package: Optional[str] = None,
):
    """
    Hauptfunktion für den Download-Prozess.

    Args:
        path (Path): Zielverzeichnis
        username (str): Benutzername
        password (str): Passwort
        version (int): Formular-Version
        uri (str): Formular-URI
        package (str, optional): Python-Paketname

    Example:
        >>> download(Path("/tmp"), "user", "pass", 2, "https://example.com/form")
    """

resolve_form_name()

def resolve_form_name(js_schema: dict, uri: str) -> str:
    """
    Ermittelt einen verwendbaren Formularnamen.

    Fällt auf den URI-Slug zurück, wenn der Server
    'unknown_form' als Namen liefert.

    Args:
        js_schema (dict): Das geparste JSON-Schema
        uri (str): Die Formular-URI

    Returns:
        str: Ein gültiger Formularname
    """

derive_name_from_uri()

def derive_name_from_uri(uri: str) -> str:
    """
    Leitet einen Formularnamen aus dem URI-Slug ab.

    Nimmt das letzte Pfad-Segment der URI und ersetzt
    Bindestriche durch Unterstriche.

    Example:
        >>> derive_name_from_uri("https://example.de/community/guvh/anfrage-zahnarzt")
        "anfrage_zahnarzt"
    """

ensure_valid_identifier()

def ensure_valid_identifier(name: str) -> str:
    """
    Stellt sicher, dass der Name ein gültiger Python-Identifier ist.

    Präfixiert Namen, die mit einer Ziffer beginnen, mit 'F_'.

    Example:
        >>> ensure_valid_identifier("90_form")
        "F_90_form"
    """

find_project_root()

def find_project_root(start_path: Path) -> Optional[Path]:
    """
    Findet das Projekt-Root anhand von Marker-Dateien
    (pyproject.toml, setup.py, setup.cfg).

    Args:
        start_path (Path): Startverzeichnis für die Suche

    Returns:
        Optional[Path]: Pfad zum Projekt-Root oder None
    """

derive_package_name()

def derive_package_name(path: Path) -> str:
    """
    Leitet den Python-Paketnamen aus dem Pfad ab.

    Erkennt automatisch src/-Layouts und berechnet den
    relativen Pfad zum Projekt-Root.

    Examples:
        /project/src/myapp/forms -> myapp.forms
        /project/myapp/forms -> myapp.forms
    """

format_version()

def format_version(version):
    """
    Formatiert Versionsnummer für Verzeichnisnamen.

    Args:
        version (int|float): Versionsnummer

    Returns:
        str: Formatierte Version (z.B. "v2_0")

    Example:
        >>> format_version(2)
        "v2_0"
        >>> format_version(2.5)
        "v2_5"
    """

main()

def main() -> int:
    """
    CLI Entry Point.

    Returns:
        int: Exit Code (0 bei Erfolg)
    """

Templates

FORM_TEMPLATE

Template für generierte Python-Formularklassen (Standard-Formulare):

FORM_TEMPLATE = """from ritter.browser import TemplateLoader
from uv.ozg.forms import DefaultDocumentEditForm as BaseOZGEditForm
from uvcreha.endpoints.browser.document import (
    DefaultDocumentEditForm as BaseREHAEditForm,
)
from uv.dynforms import dyn_form
from reha.prototypes.views.document import views

# ... Template-Inhalt
"""

WIZARD_TEMPLATE

Template für Wizard-Formulare.

WIZARD_TEMPLATE_PT / WIZARD_MODE_TEMPLATE_PT

Page-Templates für Wizard-Formulare (template.pt und wizard_template.pt).

FORM_TEST_TEMPLATE

Template für generierte Test-Dateien:

FORM_TEST_TEMPLATE = """def test_base(loaded_app):
    assert "%s" in loaded_app.document_stores["ozg"].keys()
"""

ACCEPTANCE_TEST_TEMPLATE

Template für Akzeptanz-Tests.

Konstanten

URI

Standard-URI für Schema-Abfragen:

URI = "https://formulare.uv-kooperation.de/ozg/rul/ozg-90/@@schema-view?version=1"

PROJECT_ROOT_MARKERS

Marker-Dateien zur Erkennung des Projekt-Roots:

PROJECT_ROOT_MARKERS = ("pyproject.toml", "setup.py", "setup.cfg")

HTTP Endpoints

Das Tool greift auf folgende Endpoints zu:

Endpoint Beschreibung Parameter
@@meta-schema JSON Schema des Formulars version
@@simple-ui-schema-view UI Schema des Formulars version
@@cpt-generator Page Template (Deform) version

Beispiel-Requests

# Schema abrufen
response = requests.get(
    f"{uri}/@@meta-schema?version={version}",
    auth=HTTPBasicAuth(username, password)
)

# UI Schema abrufen
response = requests.get(
    f"{uri}/@@simple-ui-schema-view?version={version}",
    auth=HTTPBasicAuth(username, password)
)

# Page Template abrufen
response = requests.get(
    f"{uri}/@@cpt-generator?version={version}",
    auth=HTTPBasicAuth(username, password)
)