# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Language

All code, comments, and docstrings must be written in **English** and formatted in google style.

## Project Overview

`ecmind_blue_client` is a Python library (Python >= 3.12) that wraps the ECMind Blue DMS (Document Management System) API. Communication with the server happens over TCP/RPC using a proprietary binary protocol.

## Commands

This project uses **UV** as the package manager.

```bash
# Create venv and install all dependencies
uv sync --extra development --extra tcp

# Run tests
uv run pytest

# Run a single test
uv run pytest tests/test_ecm_model_sync.py::TestClassName::test_method_name

# Build
uv run python -m build --no-isolation --sdist --wheel

# Format code
uv run black .
uv run isort .

# Lint
uv run pylint ecmind_blue_client
```

**Code style:** Black + isort (isort profile: `black`), max line length: **140 characters**.

**Linter:** Only **Pylint** is used — **not Ruff**. Use `# pylint: disable=<rule>` for inline suppressions, never `# noqa: PLC...` (those are Ruff-specific and have no effect here).

**Versioning:** Managed by `setuptools_scm` — the package version is derived from git tags.

**Optional extras:** `tcp` (required for TCP client), `development` (dev tools), `manage`, `objdef`, `portfolio`, `workflow` (external ECMind packages).

## Architecture

The project is structured in three layers:

### 1. Low-Level RPC (`rpc/`)
Direct TCP socket access to the Blue server. `rpc/sync_api.py` / `rpc/async_api.py` serialize job parameters into the proprietary binary protocol. `rpc/_rpc_connection.py` manages the socket connection. SSL/TLS is supported by both variants.

The server session lifecycle is: `SessionAttach` → `SessionLogin` → DMS operations → `SessionLogout`.

### 2. Mid-Level Client API (`client.py`, `pool/`)
- **`SyncPoolClient`** / **`AsyncPoolClient`** in `pool/`: Recommended connection pool implementations with weighted load balancing across multiple servers. `SyncPoolClient` uses `threading.Lock`; `AsyncPoolClient` uses `asyncio.Queue`.
- **`AbstractPoolClient`** in `pool/_abstract_pool_client.py`: Shared base class.
- **`TcpClient`** / **`TcpPoolClient`**: Single-connection and older pool approach, kept for compatibility.
- The abstract `Client` base class in `client.py` provides all high-level helper methods such as `lol_query()`, `xml_import()`, `store_in_cache()`, etc.

### 3. High-Level Object API (`ecm/`)
Object-oriented API for typed ECM objects (Folders, Registers, Documents).
- **`ecm/__init__.py`**: `ECM()` factory function — main entry point for users. It is overloaded: passing a `SyncPoolClient` returns `ECMSync`, passing an `AsyncPoolClient` returns `ECMAsync`.
- **`ecm/_ecm.py`**: Abstract base class `ECMAbstract` — shared static helpers `_build_insert_xml`, `_build_update_xml`, `_build_upsert_xml`, `_build_delete_xml`, and `_check_required_fields`.
- **`ecm/_common.py`**: Shared field value type aliases and conversion helpers (e.g. `DmsObjectFieldValueTypes`, `dms_object_field_value_to_str`).
- **`ecm/synchron/_ecm_sync.py`**: `ECMSync` — exposes `execute()`, `impersonate()`, and the namespace attributes `dms` (`ECMDmsSync`) and `security` (`ECMSecuritySync`).
- **`ecm/synchron/_ecm_dms_sync.py`**: `ECMDmsSync` — all synchronous object operations: `select()`, `insert()`, `insert_and_get()`, `update()`, `update_and_get()`, `upsert()`, `delete()`, `get()`, `get_object_type_by_id()`, `files()`. Accessed via `ecm.dms`.
- **`ecm/synchron/_ecm_security_sync.py`**: `ECMSecuritySync` — user/group management. Accessed via `ecm.security`. Methods: `roles(user_guid="")`, `users(extended_info=False)`, `groups(user_guid)`, `user(username)`.
- **`ecm/asynchron/_ecm_async.py`**: `ECMAsync` — async counterpart to `ECMSync`; exposes `execute()`, `impersonate()`, and the namespace attributes `dms` (`ECMDmsAsync`) and `security` (`ECMSecurityAsync`).
- **`ecm/asynchron/_ecm_dms_async.py`**: `ECMDmsAsync` — identical API to `ECMDmsSync` but with `async def` / `await`. Accessed via `ecm.dms`.
- **`ecm/asynchron/_ecm_security_async.py`**: `ECMSecurityAsync` — async mirror of `ECMSecuritySync`. Accessed via `ecm.security`.
- **`ecm/_ecm_adm_info_parser.py`**: `ECMAdmInfoParser` — parses the `<AdmInfo>` XML format returned by `mng.GetUserList`, `mng.GetUserGroups`, and `mng.GetUserAttributes`. Defines `ECMUser`, `ECMGroup`, and `ECMUserAttributes` dataclasses.
- **`ecm/_ecm_security_roles.py`**: `ECMSystemRole` IntEnum — all known server system role IDs, grouped by module (DMS, Admin, Editor, Start, Capture, Client, WebClient, Workflow, Enterprise Manager, Server).
- **`ecm/model/`**: Model API — `_ecm_model.py` (base classes, dataclasses), `_ecm_model_field.py` (ECMField descriptor); query builders in `model/synchron/_ecm_model_query_sync.py` and `model/asynchron/_ecm_model_query_async.py`; upsert builders in `model/synchron/_ecm_model_upsert_sync.py` and `model/asynchron/_ecm_model_upsert_async.py`.

### Model API (`ecm/model/_ecm_model.py`, `_ecm_model_field.py`)
Declarative ORM-style API for typed ECM queries. Accessed via `ecm.dms.select()` on both `ECMSync` and `ECMAsync`.

- **`_ECMModelBase`** (metaclass: `ECMModelMeta`): Shared base — stores raw field values in `_raw_fields_`, populates them from parser output via `_from_dms_parsed()`. Subclasses set `_internal_name_` or `_type_id_` to identify the server object type.
- **`ECMFolderModel`** / **`ECMRegisterModel`** / **`ECMDocumentModel`**: Concrete bases users subclass.
- **`system` namespace**: All server-populated, non-user-defined metadata is exposed via `model.system` (type `ECMModelSystem`). Properties: `id`, `name`, `rights`, `base_params`, `is_modified`, `has_removed_table_rows`, `modified_fields`, `modified_table_fields`, `is_field_modified()`. `ECMRegisterModel.system` returns `ECMRegisterModelSystem` (adds `folder_id`, `parent_register_id`); `ECMDocumentModel.system` returns `ECMDocumentModelSystem` (adds `folder_id`, `register_id`, `register_type_id`, `file_properties`).
- **`ECMModelRights`** / **`ECMModelBaseParams`** / **`ECMModelFileProperties`**: Dataclasses populated from the server response when the corresponding flag is requested on the query. `ECMModelFileProperties` is only available via `ECMDocumentModel.system.file_properties`.
- **`ECMField[T]`** in `_ecm_model_field.py`: Descriptor that behaves as a condition builder at class level (`Model.Field == value` → `ECMCondition`) and returns the stored value at instance level (`obj.Field` → `T | None`). Dynamic (undeclared) fields are accessed via `Model["internalName"]`. Supports `.ASC` / `.DESC` properties that return `ECMSortOrder` for use with `.order_by()`.
- **`ECMTableField[RowT]`**: Descriptor for multi-row table fields. Declared with an `ECMTableRowModel` subclass as the row type. At instance level returns `list[RowT]`. Row classes declare their columns as `ECMField[T]` annotations; rows expose `row_id`, `is_modified`, and `is_field_modified()`.
- **`ECMSortOrder`**: Sort directive produced by `ECMField.ASC` / `ECMField.DESC`. Passed to `.order_by()`.
- **`ECMCondition`** / **`ECMConditionGroup`**: Condition expressions; combined with `&` (AND) and `|` (OR).
- **`make_folder_model(internal_name)`** / **`make_register_model`** / **`make_document_model`**: Dynamic factories that create a model class at runtime without a class statement. Useful when the object type is only known at runtime.
- **`ECMModelQuerySync`** / **`ECMModelQueryAsync`** in `model/synchron/` and `model/asynchron/`: Fluent query builders returned by `ecm.dms.select()`. Modifier methods: `.where(*conditions)`, `.order_by(*sort_orders)`, `.limit()`, `.with_pagesize()`, `.with_offset()`, `.rights()`, `.base_params()`, `.file_properties()`. Terminal methods: `.execute()` (returns list) and `.stream()` (generator / async generator).
- **`ECMModelUpsertSync`** / **`ECMModelUpsertAsync`** in `model/synchron/` and `model/asynchron/`: Fluent upsert builders returned by `ecm.dms.upsert(model, ...)`. Wraps `dms.XMLImport`. Modifier methods: `.search(*conditions)`, `.action0()`, `.action1()`, `.action_multiple()`, `.files()`. Terminal method: `.execute()` returns `(object_id, object_type_id, hits, action)`.

```python
class InvoiceRow(ECMTableRowModel):
    Amount: ECMField[float]
    Description: ECMField[str]

class InvoiceFolder(ECMFolderModel):
    _internal_name_ = "InvoiceFolder"
    Title: ECMField[str]
    Year:  ECMField[int]
    Positions: ECMTableField[InvoiceRow]

# Sync — with sorting and rights
results = ecm.dms.select(InvoiceFolder).where(
    InvoiceFolder.Title == "Rechnung 2024",
    (InvoiceFolder.Year >= 2020) & (InvoiceFolder.Year <= 2024),
).order_by(InvoiceFolder.Year.DESC).rights().execute()

# Async
async for folder in ecm.dms.select(InvoiceFolder).stream():
    print(folder.system.id, folder.Title)

# User context switching (injects $$$SwitchContextUserName$$$ into every request)
with ecm.impersonate("john") as ecm_john:
    folder = ecm_john.dms.insert_and_get(InvoiceFolder(Title="Test"))

# Dynamic model (type only known at runtime)
InvoiceFolder = make_folder_model("InvoiceFolder")
```

### Definition module (`ecm/definition/`)
Parses the asobjdef XML (UTF-16 encoded, retrieved once from the server) into a typed object graph.

- **`ECMDefinition`**: Entry point. Accepts `str` or `JobResponseFile`. Exposes `primary_language`, `available_languages`, `languages`, and `cabinets`.
- **`ECMLanguageDefinition`**: `lang_id`, `name`, `iso_code` (e.g. `"de_DE"`), `is_primary`.
- **`ECMCabinetDefinition`**: Cabinet with `objects: dict[str, ECMObjectDefinition]` (keyed by internal name). Dict-like API: `__getitem__`, `__contains__`, `__iter__`, `keys()`, `values()`, `items()`. Convenience properties `.folders`, `.registers`, `.documents` return filtered dicts.
- **`ECMObjectDefinition`** and subclasses `ECMFolderDefinition`, `ECMRegisterDefinition`, `ECMDocumentDefinition`: Typed definitions for each `<object>` element. Common fields include `maintype` (`ECMMainType` enum), `cotype`, `internal_name`, `history_flags` (`ECMHistoryFlags`), `compression_flags` (`ECMCompressionMode`), `fulltext` (`ECMFulltextMode`). `ECMDocumentDefinition` additionally has `cross_module_document_type: bool`. Field access: `obj.fields` (top-level only), `obj.all_fields` (flattened across PageControl pages), `obj["internalName"]` (shortcut for `all_fields`).
- **`ECMDescriptionEntry`**: Language-specific name entry (`name`, `tooltip`, `image_id`, URL fields). Accessed via `obj.description` (primary language, `None` if unavailable) or `obj.descriptions["de_DE"]` (by ISO code).
- **`ECMFieldDefinition`**: Typed definition for a single field within an object type. Key properties: `internal_name`, `field_name`, `data_type` (`ECMFieldDataType`), `control` (`ECMControlType`), `catalog` (`ECMCatalogType | None`), `required`, `readonly`, `key_field`, `multiline`, `pages` (nested PageControl tabs). Static UI elements (groups, labels) have `is_static=True` and `data_type=None`.
- **`ECMPageDefinition`**: A tab within a PageControl field (`dt="C"`). Has `internal_name`, `position`, `description`, `descriptions`, and `fields: dict[str, ECMFieldDefinition]`.

```python
definition = ECMDefinition(job_response_file)
print(definition.primary_language.iso_code)   # e.g. "de_DE"
for cabinet in definition.cabinets:
    for folder in cabinet.folders:
        print(folder.internal_name, folder.description.name)
        print(folder.descriptions["en_US"].tooltip)
```

### Builder module (`ecm/builder/`)
Two distinct builder families, both based on fluent nested context managers:

**Query builders** — produce DMSQuery XML sent to the server for read operations:
- `DmsQueryBuilderHol` / `DmsQueryBuilderLol`: Top-level entry points for HOL and LOL queries.
- Shared types and literals (operators, field value types, special values, etc.) live in `ecm/builder/_common.py`.

**Data builder** — produces DMSData XML for write operations (`dms.Index` and similar):
- `DmsDataBuilder` → `DmsDataObject` → `DmsDataTableField` (nested).
- The schema is documented in `DMSData.xsd` at the repository root.

```python
# Query example (HOL)
with DmsQueryBuilderHol() as b:
    with b.object_type("Folder") as ot:
        with ot.condition_object() as co:
            co.field_condition(SystemFields.OBJECT_ID, 123)

# Write example
with DmsDataBuilder() as b:
    with b.object("InvoiceType") as obj:
        obj.field("Title", "Invoice 2024")
        obj.folder_id = 42
```

### Parser module (`ecm/parser/`)
Parses DMSContent XML responses from the server (schema: `DMSContent.xsd` at repository root).
- `DmsContentParserHol` / `DmsContentParserLol`: Entry points for HOL and LOL response parsing.
- Results are typed objects (`DmsContentObjectTypeHol`, `DmsContentObjectTypeLol`, etc.) with typed field accessors.

### Query Formats
- **HOL (Hierarchical Object List)**: Hierarchical queries with parent/child relationships.
- **LOL (List of Lists)**: Paginated flat-list queries.

### Sync vs. Async
The sync/async split is reflected in the package structure:
- `ecm/synchron/` ↔ `ecm/asynchron/`
- `ecm/model/synchron/` ↔ `ecm/model/asynchron/`

When changing one variant, always apply the equivalent change to its counterpart. Test files follow the same pairing (`test_ecm_model_sync.py` ↔ `test_ecm_model_async.py`).

## File Operations

When renaming or moving files within the repository, always use `git mv` instead of `mv` to preserve git history.

## Conventions

- Private modules are prefixed with `_` (e.g. `_ecm_sync.py`, `_dms_query_builder.py`)
- Enums for jobs, flags, and system fields are defined in `const.py`
- New ECM object types require both a sync and an async variant in `ecm/model/`
- Docstrings follow Google style with `Args:`, `Returns:`, `Raises:`
- Use `@overload` decorators for type-safe polymorphic methods
- `__exit__` methods on context-manager classes use `exc_type, exc_value, traceback` as parameter names (protocol-required, unused by design — consistent across the entire codebase)

## Circular Import Warning

The `ecm/synchron/` and `ecm/asynchron/` subpackages are prone to circular imports with `ecm/model/`. Cross-subpackage imports (e.g. from `_ecm_dms_sync.py` into `ecm/model/`) **must** use lazy imports inside functions, not top-level imports:

```python
# Correct — lazy import inside function body breaks circular dependency
def select(self, model_class):
    from ecmind_blue_client.ecm.model.synchron._ecm_model_query_sync import ECMModelQuerySync  # pylint: disable=import-outside-toplevel
    ...

# Wrong — top-level import causes ImportError on partially initialized module
from ecmind_blue_client.ecm.model.synchron._ecm_model_query_sync import ECMModelQuerySync
```

`ECMDmsSync` / `ECMDmsAsync` import their owning `ECMSync` / `ECMAsync` class only under `TYPE_CHECKING` (to break the mutual dependency). Use `# type: ignore[arg-type]` when passing `self` from the owning class constructor (`ECMDmsSync(self)`) to suppress Pyright's `Self` vs concrete-type mismatch.

## Exception Hierarchy

- `ECMException`: Base class for all ECM-level errors
  - `ECMNotFoundException`: Requested ECM object does not exist
  - `ECMWrongStateException`: Invalid state transition on an ECM object
- `BlueException`: Server-side errors from the RPC layer
- `BlueProtocolException`: Binary protocol violations

## Deprecated

`ecmind_blue_client.com_client` and `ecmind_blue_client.soap_client` are no longer supported. `TcpClient` and `TcpPoolClient` still work but are superseded by `SyncPoolClient` / `AsyncPoolClient`.
