= ecm-generate-models
:description: CLI tool ecm-generate-models: generates typed Python model classes (ECMFolderModel, ECMRegisterModel, ECMDocumentModel) from a live enaio server or a local ObjDef XML file.
:keywords: ecm-generate-models, CLI, model classes, ECMFolderModel, ECMRegisterModel, ECMDocumentModel, ObjDef, enaio
:page-tags: guide

`ecm-generate-models` is a command-line tool that generates typed Python model classes (`ECMFolderModel`, `ECMRegisterModel`, `ECMDocumentModel`) from the enaio object definition.

The generated classes contain a typed `ECMField` declaration for every field, plus enums for list catalog fields. This gives full code completion and type checking in the IDE without having to maintain field names manually.

== Installation

The command is included in the base installation — no additional extra required:

[tabs]
====
uv::
+
[source,bash]
----
uv add ecmind-blue-client
----

pip::
+
[source,bash]
----
pip install ecmind-blue-client
----
====

== Arguments

=== Source (exactly one required)

[cols="1,1,3"]
|===
|Argument |Default |Description

|`--host HOST`
|—
|Hostname or IP address of the enaio server. Mutually exclusive with `--file`.

|`--file XML_FILE`
|—
|Path to a local `asobjdef` XML file (UTF-16 or UTF-8 encoded). Mutually exclusive with `--host`.
|===

=== Server options (used with `--host`)

[cols="1,1,3"]
|===
|Argument |Default |Description

|`--port PORT`
|`4000`
|TCP port of the enaio server.

|`--username USER`
|—
|Login username. Required with `--host`.

|`--password PASS`
|—
|Login password. Required with `--host`.

|`--no-ssl`
|SSL enabled
|Disable SSL/TLS. Default: SSL is enabled.

|`--name APPNAME`
|—
|Application name shown in Enterprise Manager (optional).
|===

=== Output

[cols="1,1,3"]
|===
|Argument |Default |Description

|`--output-dir DIR`
|stdout
|Directory to write generated `.py` files into. One file per cabinet: `<internal_name>.py`. If omitted, the generated code is printed to stdout.

|`--cabinet NAME`
|all
|Only generate models for the cabinet with this internal name. If omitted, all cabinets are generated.
|===

== Examples

=== Generate from a live server

The most common use case: connect to the server and write all cabinets into a directory.

[source,bash]
----
ecm-generate-models \
    --host enaio.example.com \
    --username admin \
    --password secret \
    --output-dir ./models
----

Terminal output:

[source]
----
Written: models/Invoices.py
Written: models/Contracts.py
----

=== Generate only a specific cabinet

[source,bash]
----
ecm-generate-models \
    --host enaio.example.com \
    --username admin \
    --password secret \
    --cabinet Invoices \
    --output-dir ./models
----

=== Generate from a local XML file

The `asobjdef` file can be exported once from the server and reused locally — no active server connection required.

[source,bash]
----
ecm-generate-models --file asobjdef.xml --output-dir ./models
----

=== Preview on the console

Without `--output-dir`, the generated code is printed directly — useful for a quick inspection:

[source,bash]
----
ecm-generate-models \
    --host enaio.example.com \
    --username admin \
    --password secret \
    --cabinet Invoices
----

=== Without SSL (development environment)

[source,bash]
----
ecm-generate-models \
    --host localhost \
    --port 4000 \
    --username admin \
    --password secret \
    --no-ssl \
    --output-dir ./models
----

== Output format

One `.py` file is generated per cabinet with the following structure:

* Imports (only those actually needed)
* Enums for fields with list catalogs
* `ECMTableRowModel` subclasses for table fields
* Model classes (`ECMFolderModel`, `ECMRegisterModel`, `ECMDocumentModel`)

Example of a generated file:

[source,python]
----
"""Auto-generated ECM model classes for cabinet "Invoices".

DO NOT EDIT — regenerate with ecm-generate-models.
"""

from enum import Enum
from datetime import date

from ecmind_blue_client.ecm.model import (
    ECMDocumentModel,
    ECMField,
    ECMFolderModel,
    ECMRegisterModel,
    ECMTableField,
    ECMTableRowModel,
)


class InvoiceFolder_StatusEnum(str, Enum):
    OPEN = "Open"
    PAID = "Paid"
    CANCELLED = "Cancelled"


class InvoiceDocument_PositionsRow(ECMTableRowModel):
    Description: ECMField[str] = ECMField(str, default=None)
    Amount: ECMField[float] = ECMField(float, default=None)
    Date: ECMField[date] = ECMField(date, default=None)


class InvoiceFolder(ECMFolderModel):
    _internal_name_ = "InvoiceFolder"

    Title: ECMField[str] = ECMField(str, mandatory=True, default=None)
    Year: ECMField[int] = ECMField(int, default=None)
    Status: ECMField[InvoiceFolder_StatusEnum] = ECMField(InvoiceFolder_StatusEnum, default=None)


class InvoiceRegister(ECMRegisterModel):
    _internal_name_ = "InvoiceRegister"

    Name: ECMField[str] = ECMField(str, mandatory=True, default=None)


class InvoiceDocument(ECMDocumentModel):
    _internal_name_ = "InvoiceDocument"

    Title: ECMField[str] = ECMField(str, mandatory=True, default=None)
    InvoiceDate: ECMField[date] = ECMField(date, default=None)
    Positions: ECMTableField[InvoiceDocument_PositionsRow] = ECMTableField(default_factory=list)
----

== Using the generated models

The generated files are imported once after creation and then used throughout the application:

[tabs]
====
Sync::
+
[source,python]
----
from ecmind_blue_client.ecm import ECM
from ecmind_blue_client.pool import SyncPoolClient
from models.Invoices import InvoiceFolder, InvoiceRegister, InvoiceDocument

ecm = ECM(SyncPoolClient(servers="enaio.example.com:4000:1", username="admin", password="secret"))

# Query with full type support
for folder in (
    ecm.dms.select(InvoiceFolder)
    .where(InvoiceFolder.Year >= 2024)
    .order_by(InvoiceFolder.Year.DESC)
    .stream()
):
    print(folder.system.id, folder.Title, folder.Status)

# Create a new object
folder = ecm.dms.insert_and_get(
    InvoiceFolder(Title="Invoice 2024", Year=2024)
)

# Update a field
folder.Title = "Invoice 2024 (updated)"
ecm.dms.update(folder)
----

Async::
+
[source,python]
----
from ecmind_blue_client.ecm import ECM
from ecmind_blue_client.pool import AsyncPoolClient
from models.Invoices import InvoiceFolder, InvoiceRegister, InvoiceDocument

ecm = ECM(AsyncPoolClient(servers="enaio.example.com:4000:1", username="admin", password="secret"))

# Query with full type support
async for folder in (
    ecm.dms.select(InvoiceFolder)
    .where(InvoiceFolder.Year >= 2024)
    .order_by(InvoiceFolder.Year.DESC)
    .stream()
):
    print(folder.system.id, folder.Title, folder.Status)

# Create a new object
folder = await ecm.dms.insert_and_get(
    InvoiceFolder(Title="Invoice 2024", Year=2024)
)

# Update a field
folder.Title = "Invoice 2024 (updated)"
await ecm.dms.update(folder)
----
====

== When to regenerate

The generated files should be recreated when:

* New fields or object types have been added to the enaio archive schema
* Field types or mandatory field flags have changed
* Entries in list catalogs have been added or removed

[TIP]
====
The files carry the comment `DO NOT EDIT` — manual changes will be lost the next time the command is run.
Custom extensions can be defined as subclasses in separate files.
====

== See also

* xref:quickstart.adoc[Getting started] — overview of first steps with the library
* xref:api/dms/select.adoc[select()] — query objects with a typed model
* xref:api/dms/insert.adoc[insert()] — create new objects
