select() and select_lol()
Both methods return a chainable query builder for the same typed models, over two different server query formats:
select()(HOL, Hierarchical Object List)-
Returns an
ECMModelQuerySync/ECMModelQueryAsync. The server answers with an object-based XML tree, which is what carries the hierarchy and every optional extra: file information, base parameters, variants, remarks, child and parent objects. select_lol()(LOL, Linear Object List)-
Returns an
ECMModelQueryLolSync/ECMModelQueryLolAsync. The server answers with a compact, column-oriented rowset, which is typically faster for large flat result sets but carries neither the hierarchy nor those extras.
Conditions, sorting, paging, field restriction, full-text search, rights and the hit list transport behave identically in both modes, so everything on this page applies to both unless it is marked HOL only or LOL only.
ECM Model reference — for a full description of all system properties
(system.id, system.rights, system.base_params, system.file_properties, etc.) and how change
tracking works.
1. Choosing a mode
| Capability | select() (HOL) |
select_lol() (LOL) |
|---|---|---|
Performance |
Slower |
Faster for large, flat result sets |
Sorting |
Available |
Available |
|
Available |
Not available |
|
Available (structured) |
Not available (only via system fields) |
|
Available |
Not available |
|
Available |
Ignored (server does not return icons in LOL mode) |
|
Available |
Not available |
Table field values |
Typed, with |
Untyped raw strings, |
|
If file information, base parameters, variants, or hierarchical queries are needed, |
In practice the mode follows from what the result has to carry:
-
Sync
-
Async
# Recommended for: large sets, index data only (no variants/file properties/hierarchy)
for doc in (
ecm.dms.select_lol(InvoiceDocument)
.where(InvoiceDocument.Year == 2024)
.order_by(InvoiceDocument.Title.ASC)
.pagesize(500)
.stream()
):
print(doc.system.id, doc.Title)
# Recommended for: file info, base params, variants, hierarchies → use select()
for doc in (
ecm.dms.select(InvoiceDocument)
.where(InvoiceDocument.Year == 2024)
.file_properties()
.stream()
):
print(doc.system.file_properties.extension)
# Recommended for: large sets, index data only (no variants/file properties/hierarchy)
async for doc in (
ecm.dms.select_lol(InvoiceDocument)
.where(InvoiceDocument.Year == 2024)
.order_by(InvoiceDocument.Title.ASC)
.pagesize(500)
.stream()
):
print(doc.system.id, doc.Title)
# Recommended for: file info, base params, variants, hierarchies → use select()
async for doc in (
ecm.dms.select(InvoiceDocument)
.where(InvoiceDocument.Year == 2024)
.file_properties()
.stream()
):
print(doc.system.file_properties.extension)
2. Signature
Both builders are created synchronously, in the async variant as well. The await belongs on the
terminal call (.execute()) or on the iteration (async for … .stream()).
-
Sync
-
Async
ecm.dms.select(model_class: type[T]) -> ECMModelQuerySync[T]
ecm.dms.select_lol(model_class: type[T]) -> ECMModelQueryLolSync[T]
ecm.dms.select(model_class: type[T]) -> ECMModelQueryAsync[T]
ecm.dms.select_lol(model_class: type[T]) -> ECMModelQueryLolAsync[T]
3. Parameters
| Name | Type | Description |
|---|---|---|
|
|
The model class describing the object type. Can also be created dynamically via |
4. Query builder methods
The builder methods are chainable. The Mode column says where a method exists:
| Method | Mode | Description |
|---|---|---|
|
both |
Add filter conditions. Multiple arguments are combined with AND. Conditions can be combined with |
|
both |
Set the sort order. Argument position determines sort priority. Each argument is an |
|
both |
Maximum total number of results across all pages (LOL: maps to |
|
both |
Number of objects per server request (default: 1000). Affects efficiency for large result sets. |
|
both |
Zero-based start offset. Skips the first |
|
both |
Return only the specified fields (sets |
|
both |
Add a |
|
both |
Return only objects from the recycle bin. |
|
both |
Include access rights data (populates |
|
both |
Request the hit list as a response file instead of the |
|
both |
Execute the query and return all results as a list (all pages in memory). |
|
both |
Execute the query page by page and return a generator. Recommended for large result sets. |
|
HOL only |
Include audit metadata (populates |
|
HOL only |
Include file properties (populates |
|
HOL only |
Include document variant data. |
|
HOL only |
Include remarks for returned objects. |
|
HOL only |
Include icon IDs for returned objects. In LOL mode the call is ignored, because the server returns no icons there. |
|
HOL only |
Switch to a HOL query that includes child or parent objects. Returns an |
|
|
5. Filter conditions (where)
Conditions are passed to .where(). Multiple arguments in a single .where() call are automatically combined with AND. For OR combinations, conditions are joined with the | operator; for AND with &.
|
Every comparison needs its own parentheses
The keywords
The same applies to |
|
The condition value must match the declared field type
On a generated model the comparison operators are typed against the field: an
Two calibrations keep working code working: a Dynamic models from |
5.1. Comparison operators
| Syntax | Operator | Example |
|---|---|---|
|
Equality |
|
|
Inequality |
|
|
Less than |
|
|
Less than or equal |
|
|
Greater than |
|
|
Greater than or equal |
|
|
Matches one of the values |
|
|
Matches none of the values |
|
|
Between two values (inclusive) |
|
5.2. AND combinations
Multiple arguments in .where() are combined with AND. The & operator can also be used explicitly:
# Variant 1: multiple arguments → AND
ecm.dms.select(InvoiceFolder).where(
InvoiceFolder.Year >= 2020,
InvoiceFolder.Status == "Approved",
)
# Variant 2: explicit & → same result
ecm.dms.select(InvoiceFolder).where(
(InvoiceFolder.Year >= 2020) & (InvoiceFolder.Status == "Approved")
)
5.3. OR combinations
Conditions are combined into an OR group using |:
ecm.dms.select(InvoiceFolder).where(
(InvoiceFolder.Status == "Open") | (InvoiceFolder.Status == "In Progress")
)
For value sets, .in_() is more concise:
ecm.dms.select(InvoiceFolder).where(
InvoiceFolder.Status.in_("Open", "In Progress")
)
5.4. Mixed AND/OR groups
& and | can be nested arbitrarily. Python parentheses control the evaluation order:
-
Sync
-
Async
# (Year >= 2020 AND Year <= 2024) AND (Status = "Open" OR Status = "In Progress")
for folder in (
ecm.dms.select(InvoiceFolder)
.where(
(InvoiceFolder.Year >= 2020) & (InvoiceFolder.Year <= 2024),
InvoiceFolder.Status.in_("Open", "In Progress"),
)
.stream()
):
print(folder.Title, folder.Year)
async for folder in (
ecm.dms.select(InvoiceFolder)
.where(
(InvoiceFolder.Year >= 2020) & (InvoiceFolder.Year <= 2024),
InvoiceFolder.Status.in_("Open", "In Progress"),
)
.stream()
):
print(folder.Title, folder.Year)
5.5. between()
.between(lower, upper) is a compact alternative to >= + ⇐:
-
Sync
-
Async
for folder in (
ecm.dms.select(InvoiceFolder)
.where(InvoiceFolder.Year.between(2020, 2024))
.stream()
):
print(folder.Title)
async for folder in (
ecm.dms.select(InvoiceFolder)
.where(InvoiceFolder.Year.between(2020, 2024))
.stream()
):
print(folder.Title)
5.6. Table-field column conditions
The conditions work in both modes. What differs is the result: LOL returns table-field values as untyped raw strings and row.row_id is always None, while HOL returns them typed. Table field values are returned as untyped raw strings and are not converted to the declared Python type. row_id is always None for table rows returned from LOL queries.
|
Conditions on a table-field column (a column of a sub-table / multi-field) are written by accessing
the column on the model’s ECMTableField (e.g. Invoice.Positions). The builder then emits a
<TableCondition> / <TableColumn> rather than a flat field condition, so the server filters on the
table column instead of rejecting an unknown object field.
# any row whose ArticleNo == "A-100"
ecm.dms.select(Invoice).where(Invoice.Positions.ArticleNo == "A-100").execute()
produces:
<ConditionObject internal_name="Invoice">
<TableCondition internal_name="Positions">
<TableColumn internal_name="ArticleNo" operator="=">
<Value>A-100</Value>
</TableColumn>
</TableCondition>
</ConditionObject>
All comparison and collection operators work (==, !=, <, ⇐, >, >=, .in_(),
.not_in(), .between()), and a table-column condition can be combined with ordinary field
conditions in the same .where() call.
To restrict the condition to a single row, subscript the table field with the server row number —
Invoice.Positions[3] (the explicit alias Invoice.Positions.row(3) is equivalent):
# Quantity in row 3 == 5
ecm.dms.select(Invoice).where(Invoice.Positions[3].Quantity == 5).execute()
<TableCondition internal_name="Positions" row="3">
<TableColumn internal_name="Quantity" operator="="><Value>5</Value></TableColumn>
</TableCondition>
|
Subscripting is class-level query syntax. At instance level |
5.7. Combined queries across multiple object types
HOL only. select_lol() offers no equivalent, see Choosing a mode.
|
Conditions in .where() can reference fields from different object types within the same cabinet. The server returns only those objects where all conditions are satisfied — regardless of which object type a condition refers to.
The following combinations are supported:
-
Search for a document with conditions on fields of the document itself, its register, and its folder.
-
Search for a register or folder that contains a child object matching a given condition.
|
When registers are nested, only the immediately enclosing register is available; parent registers above it are not. |
Search for a document — with conditions on register and folder:
-
Sync
-
Async
from tests.models.Unittest_DMS import Unittest_DMS, Unittest_DMS_Register, Unittest_DMS_Document
for doc in (
ecm.dms.select(Unittest_DMS_Document)
.where(
Unittest_DMS_Document.StringField == "Invoice",
Unittest_DMS_Register.Name == "Incoming invoices",
Unittest_DMS.Name == "Supplier GmbH",
)
.stream()
):
print(doc.system.id, doc.Name)
async for doc in (
ecm.dms.select(Unittest_DMS_Document)
.where(
Unittest_DMS_Document.StringField == "Invoice",
Unittest_DMS_Register.Name == "Incoming invoices",
Unittest_DMS.Name == "Supplier GmbH",
)
.stream()
):
print(doc.system.id, doc.Name)
Search for a folder — that contains a document matching a condition:
-
Sync
-
Async
from tests.models.Unittest_DMS import Unittest_DMS, Unittest_DMS_Document
for folder in (
ecm.dms.select(Unittest_DMS)
.where(
Unittest_DMS.Name == "Supplier GmbH",
Unittest_DMS_Document.StringField == "Invoice",
)
.stream()
):
print(folder.system.id, folder.Name)
async for folder in (
ecm.dms.select(Unittest_DMS)
.where(
Unittest_DMS.Name == "Supplier GmbH",
Unittest_DMS_Document.StringField == "Invoice",
)
.stream()
):
print(folder.system.id, folder.Name)
5.8. System fields as conditions
Besides index fields, system fields can also be used in .where() (and .order_by()). For this the system attribute is access-aware:
-
Instance (
obj.system.id) returns the loaded value. -
Class (
Model.system.id) returns anECMFieldusable in conditions like an index field.
This lets you use the same dotted path inside a query. Most common case — load all documents inside a known folder via a cross-type condition on the folder ID (building on the previous section):
-
Sync
-
Async
docs = (
ecm.dms.select(Unittest_DMS_Document)
.where(Unittest_DMS.system.id == folder.system.id) # left: class (ECMField), right: instance value (int)
.execute()
)
docs = await (
ecm.dms.select(Unittest_DMS_Document)
.where(Unittest_DMS.system.id == folder.system.id)
.execute()
)
The condition is bound to the object type it is called on (Unittest_DMS) and placed into its own ConditionObject like any cross-type condition. System fields automatically receive the system="1" attribute — identical to Unittest_DMS["OBJECT_ID"]:
<ConditionObject internal_name="Unittest_DMS">
<FieldCondition internal_name="OBJECT_ID" operator="=" system="1">
<Value>4711</Value>
</FieldCondition>
</ConditionObject>
Only system fields backed by a single column are queryable. Available on all object types: id, owner_guid, creator, creation_date, last_modifier, last_modified, creation_time, deleted_at, plus the subset system.base_params.{creator, creation_date, modifier, modified_date, links_count, locked_user_id}. Type-dependent: system.folder_id, system.parent_register_id (register); system.folder_id, system.register_id, system.register_type_id, system.system_id, system.foreign_id, system.lock_user_id, system.archivist, system.archiving_date and system.file_properties.{count, size} (document).
Generic alternative — any system field via item access: Model["NAME"] (subscript on the model class) returns an ECMField for any field name. If NAME is a SystemFields member, system="1" is set automatically. This also covers system fields without a .system property (OBJECT_FLAGS, OBJECT_RETENTION, …) and works for dynamic models without declared fields:
ecm.dms.select(Unittest_DMS_Document).where(Unittest_DMS["OBJECT_ID"] == folder.system.id) # = Unittest_DMS.system.id
ecm.dms.select(Unittest_DMS_Document).where(Unittest_DMS["OBJECT_FLAGS"] == 0)
Item access is untyped (ECMField[str]); Model.system.<field> is typed and discoverable but limited to the curated subset. Both produce the same condition.
Also as a sort field: The same system fields work in .order_by() (HOL and LOL). The result field element automatically receives system="1":
ecm.dms.select(Unittest_DMS_Document).order_by(Unittest_DMS_Document.system.id.DESC)
<Field internal_name="OBJECT_ID" system="1" sortpos="1" sortorder="DESC"/>
|
Aggregate values without a queryable server column ( |
6. Full-text search
The per-object-type flavour shown here works in both modes. The archive-wide variant (<FulltextQuery>, hits across several object types) is HOL only.
|
.fulltext(term) adds a <Fulltext> condition to the ConditionObject of the queried object type. It is an additional restriction and can be combined with .where(…). Requires a configured full-text engine on the enaio® server, with the object type indexed.
|
If no search term is supplied or no search engine is configured, the server responds with an error in the |
-
Sync
-
Async
for doc in (
ecm.dms.select(MedicalLetter)
.fulltext("Meningitis")
.where(MedicalLetter.SeniorPhysician == "Müller")
.stream()
):
print(doc.system.id, doc.Type)
async for doc in (
ecm.dms.select(MedicalLetter)
.fulltext("Meningitis")
.where(MedicalLetter.SeniorPhysician == "Müller")
.stream()
):
print(doc.system.id, doc.Type)
6.1. RetrievalWare engine parameters
Optional RetrievalWare engine attributes can be passed as keyword arguments. They only take effect when RetrievalWare is the full-text engine in use on the server. Parameters left as None use the server defaults.
| Parameter | Server default | Description |
|---|---|---|
|
|
Search mode. Allowed values: |
|
|
Word expansion level for thesaurus lookups. |
|
|
Enable fuzzy spelling for half-words. |
|
|
Similarity threshold for fuzzy spelling. |
|
|
Maximum number of fuzzy spelling hits. |
|
|
Maximum number of regular expression expansions. |
|
|
Emit a warning when the expansion limit is reached. |
|
|
Maximum number of word expansions. |
ecm.dms.select(MedicalLetter).fulltext(
"Meningitis OR Encephalitis",
mode="BOOLEAN",
expansion_level=2,
).execute()
7. Sorting
The sort order is set via .order_by(). Each argument is an ECMSortOrder produced by .ASC or .DESC on an ECMField at class level. Argument position determines sort priority.
-
Sync
-
Async
# Primary sort by year descending, secondary by title ascending
for folder in (
ecm.dms.select(InvoiceFolder)
.order_by(InvoiceFolder.Year.DESC, InvoiceFolder.Title.ASC)
.stream()
):
print(folder.Year, folder.Title)
async for folder in (
ecm.dms.select(InvoiceFolder)
.order_by(InvoiceFolder.Year.DESC, InvoiceFolder.Title.ASC)
.stream()
):
print(folder.Year, folder.Title)
7.1. Keyset pagination with system fields
.fields() and .order_by() also accept system fields via class-level access
(Model.system.<field>, see the model reference).
System fields automatically receive the system="1" attribute in the generated <Field>
element; a field that is both selected and sorted on produces a single <Field> entry.
This enables, for example, stable keyset pagination over the object ID:
-
Sync
-
Async
cursor: int | None = None
batch = 200
while True:
query = (
ecm.dms.select_lol(InvoiceDocument)
.fields(InvoiceDocument.system.id)
.order_by(InvoiceDocument.system.id.DESC)
.limit(batch)
.pagesize(batch)
)
if cursor is not None:
query = query.where(InvoiceDocument.system.id < cursor)
docs = query.execute()
if not docs:
break
for doc in docs:
... # process batch
cursor = docs[-1].system.id
cursor: int | None = None
batch = 200
while True:
query = (
ecm.dms.select_lol(InvoiceDocument)
.fields(InvoiceDocument.system.id)
.order_by(InvoiceDocument.system.id.DESC)
.limit(batch)
.pagesize(batch)
)
if cursor is not None:
query = query.where(InvoiceDocument.system.id < cursor)
docs = await query.execute()
if not docs:
break
for doc in docs:
... # process batch
cursor = docs[-1].system.id
8. Pagination
.limit(), .pagesize() and .offset() restrict the result set and control internal page navigation.
| Method | Default | Description |
|---|---|---|
|
unlimited |
Maximum total number of results across all pages. |
|
1000 |
Number of objects per server request. Smaller values reduce per-page memory usage but increase the number of requests. Large values can overload the server — see the warning below. |
|
0 |
Skip the first |
-
Sync
-
Async
# Page 3 with 20 entries each (offset = 2 × 20 = 40), sorted by year descending
for folder in (
ecm.dms.select(InvoiceFolder)
.order_by(InvoiceFolder.Year.DESC)
.limit(20)
.offset(40)
.stream()
):
print(folder.system.id, folder.Title)
async for folder in (
ecm.dms.select(InvoiceFolder)
.order_by(InvoiceFolder.Year.DESC)
.limit(20)
.offset(40)
.stream()
):
print(folder.system.id, folder.Title)
|
A pagesize that is too large can cause the server to crash. Memory consumption per page grows proportionally to the number of objects multiplied by the requested metadata. The combination of large pages with As a rule of thumb: keep the default of 1000 and only adjust when a proven performance problem exists — and then reduce rather than increase. |
9. Restricting fields (fields)
| The restriction applies to index fields. The standard system fields, among them the location fields of the object, stay in the request in both modes, see Folder and register of a hit. |
.fields() loads only the specified index fields from the server. The server internally sets field_schema="MIN" and transfers only the explicitly listed fields. System fields (system.id, system.name, etc.) and sort fields are always included regardless of this list.
This significantly reduces the amount of data transferred when only a few fields are needed. Fields that were not requested are None on the returned object.
Fields can be passed as ECMField class attributes or as internal field name strings. Calling without arguments resets the restriction and returns all fields again.
-
Sync
-
Async
# Load only Title and Year — all other fields are None
for folder in (
ecm.dms.select(InvoiceFolder)
.fields(InvoiceFolder.Title, InvoiceFolder.Year)
.stream()
):
print(folder.Title, folder.Year)
# folder.Status is None (not loaded)
async for folder in (
ecm.dms.select(InvoiceFolder)
.fields(InvoiceFolder.Title, InvoiceFolder.Year)
.stream()
):
print(folder.Title, folder.Year)
Fields can alternatively be passed as strings when no typed model is available:
-
Sync
-
Async
from ecmind_blue_client.ecm.model import make_folder_model
InvoiceFolder = make_folder_model("InvoiceFolder")
for folder in (
ecm.dms.select(InvoiceFolder)
.fields("Title", "Year")
.stream()
):
print(folder["Title"], folder["Year"])
async for folder in (
ecm.dms.select(InvoiceFolder)
.fields("Title", "Year")
.stream()
):
print(folder["Title"], folder["Year"])
10. Hit list transport
dms.GetResultList returns the hit list in one of two ways: inline as the BASE64 parameter
XML (Flags=0), or as a response file (Flags=16). The client asks for the parameter by
default because that saves one network round trip per page: the server writes parameter
block and result in a single pass, whereas the file variant makes it write twice and the
client wait for the 32-byte file header.
Measured against a test server at 20 ms RTT:
| Scenario | Response file | Parameter | Difference |
|---|---|---|---|
First page, |
94.5 ms |
53.3 ms |
-44 % |
1471 rows, |
13,020 ms |
8,238 ms |
-37 % |
1471 rows, |
339 ms |
278 ms |
-18 % |
Page size matters far more than the transport: the same 1471 rows cost 146 server requests
at pagesize(10) and two at pagesize(1000). The default of 1000 is deliberately high, and
lowering it is the most expensive mistake available in a list query.
.result_as_file() remains for the case where a single page grows beyond the pool client’s
file_cache_byte_limit (32 MiB by default). The RPC layer then spools the response to a
temporary file, and .stream() / .execute() read it incrementally from there, so the hit
list never has to fit in memory. Below that limit the file is buffered in memory anyway and
the option only costs the extra round trip.
# default: inline, one round trip per page
for folder in ecm.dms.select_lol(InvoiceFolder).pagesize(1000).stream():
...
# very large single page: via a temporary file, constant memory
for folder in ecm.dms.select_lol(InvoiceFolder).pagesize(200_000).result_as_file().stream():
...
|
What an incremental read cannot reach is everything that follows the hits in document order,
namely |
11. Rights information
.rights() retrieves the access rights of the logged-in user for each object. The rights are available in obj.system.rights as an ECMModelRights instance.
.rights() additionally requests the insert quotas (object_inserts), because the server otherwise reports the insert right as denied for every object. This costs further server queries per object and needs to be accounted for with large pages.
| Attribute | Description |
|---|---|
|
May create child objects (registers/documents in a folder, documents in a register). |
|
May change the index fields of the object. |
|
May read the file of the document. |
|
May modify the file of the document. |
|
May delete the object. |
-
Sync
-
Async
for folder in ecm.dms.select(InvoiceFolder).rights().stream():
r = folder.system.rights
if r.edit_metadata:
print(f"{folder.system.id}: editing allowed")
if not r.delete:
print(f"{folder.system.id}: deletion not permitted")
async for folder in ecm.dms.select(InvoiceFolder).rights().stream():
r = folder.system.rights
if r.edit_metadata:
print(f"{folder.system.id}: editing allowed")
if not r.delete:
print(f"{folder.system.id}: deletion not permitted")
12. Base parameters
In LOL mode the same values are reachable as system fields:
.base_params() is not available — obj.system.base_params is always None. Audit metadata is only accessible when the corresponding system fields are declared via ecm_system_fields on the model:
| System field | Meaning |
|---|---|
|
Creator ID |
|
Creation date |
|
Owner GUID |
|
Last modifier |
|
Last modification date |
|
Link count |
|
Text note count |
HOL only. select_lol() offers no equivalent, see Choosing a mode.
|
.base_params() retrieves administrative information stored by the server for each object. The data is available in obj.system.base_params as an ECMModelBaseParams instance.
| Attribute | Description |
|---|---|
|
Username of the creator. |
|
Date of creation. |
|
Current owner of the object. |
|
Username of the last modifier. |
|
Timestamp of the last modification. |
|
Number of links. |
|
Number of text notices. |
-
Sync
-
Async
for folder in ecm.dms.select(InvoiceFolder).base_params().stream():
bp = folder.system.base_params
print(f"Created by {bp.creator} on {bp.creation_date}")
print(f"Last modified by {bp.modifier} on {bp.modified_date}")
async for folder in ecm.dms.select(InvoiceFolder).base_params().stream():
bp = folder.system.base_params
print(f"Created by {bp.creator} on {bp.creation_date}")
print(f"Last modified by {bp.modifier} on {bp.modified_date}")
13. File properties
In LOL mode file information is limited to what the system fields carry:
.file_properties() is not available. Partial file metadata is only accessible via system fields explicitly declared on the model:
| System field | Meaning |
|---|---|
|
File size in bytes |
|
Number of files |
|
Number of document pages |
File extension, MIME type, and MIME type group are never returned by the server in LOL mode.
HOL only. select_lol() offers no equivalent, see Choosing a mode.
|
.file_properties() retrieves metadata about the file of a document. Only available for ECMDocumentModel types; obj.system.file_properties returns an ECMModelFileProperties instance.
| Attribute | Description |
|---|---|
|
Number of files (primary and secondary files). |
|
File size in bytes. |
|
File extension (e.g. |
|
MIME type (e.g. |
|
MIME group (e.g. |
|
ID of the file type icon. |
|
Number of pages (if known). |
-
Sync
-
Async
for doc in ecm.dms.select(InvoiceDocument).file_properties().stream():
fp = doc.system.file_properties
print(f"{doc.system.id}: {fp.extension}, {fp.size} bytes, {fp.documentpagecount} pages")
async for doc in ecm.dms.select(InvoiceDocument).file_properties().stream():
fp = doc.system.file_properties
print(f"{doc.system.id}: {fp.extension}, {fp.size} bytes, {fp.documentpagecount} pages")
14. Variants
HOL only. select_lol() offers no equivalent, see Choosing a mode.
|
.variants() retrieves the version branches of a document from the W-module. Only relevant for document types with the W-module enabled. The result is available in obj.system.variants as a list of ECMModelDocumentVariant instances.
Each variant has the attributes doc_id, doc_ver, is_active, doc_parent, children and level (nesting depth, 0 for the entries in obj.system.variants).
-
Sync
-
Async
for doc in ecm.dms.select(InvoiceDocument).variants().stream():
for variant in doc.system.variants:
active = "✓" if variant.is_active else " "
print(f"[{active}] {variant.doc_ver} (ID {variant.doc_id})")
async for doc in ecm.dms.select(InvoiceDocument).variants().stream():
for variant in doc.system.variants:
active = "✓" if variant.is_active else " "
print(f"[{active}] {variant.doc_ver} (ID {variant.doc_id})")
15. Remarks
HOL only. select_lol() offers no equivalent, see Choosing a mode.
|
.remarks() includes the text notices of an object in the response. The data is available in obj.system.remarks.
-
Sync
-
Async
for folder in ecm.dms.select(InvoiceFolder).remarks().stream():
for remark in folder.system.remarks:
print(remark)
async for folder in ecm.dms.select(InvoiceFolder).remarks().stream():
for remark in folder.system.remarks:
print(remark)
16. Recycle bin
.garbage_mode() restricts the query to deleted objects. Without this call only non-deleted objects are returned.
-
Sync
-
Async
# List all deleted invoice folders
for folder in ecm.dms.select(InvoiceFolder).garbage_mode().stream():
print(f"Deleted: {folder.system.id} – {folder.Title}")
async for folder in ecm.dms.select(InvoiceFolder).garbage_mode().stream():
print(f"Deleted: {folder.system.id} – {folder.Title}")
17. Folder and register of a hit
The location system fields of a document (SDSTA_ID, SDREG_ID, SDREG_TYPE) are part of the
standard field set of both modes, so they need no .fields() entry and are readable through the
system namespace of each hit:
-
Sync
-
Async
for document in ecm.dms.select_lol(InvoiceDocument).stream():
print(document.system.id, document.system.folder_id, document.system.register_id)
async for document in ecm.dms.select_lol(InvoiceDocument).stream():
print(document.system.id, document.system.folder_id, document.system.register_id)
That also holds when .fields() narrows the result: it restricts the index fields, while the system
block stays as it is. On a register model the equivalent fields are REG_STAID
(system.folder_id) and REG_PARID (system.parent_register_id).
The same fields work as conditions and as sort criteria, for example every document filed in one known folder:
-
Sync
-
Async
documents = (
ecm.dms.select_lol(InvoiceDocument)
.where(InvoiceDocument.system.folder_id == folder.system.id)
.order_by(InvoiceDocument.system.id.ASC)
.execute()
)
documents = await (
ecm.dms.select_lol(InvoiceDocument)
.where(InvoiceDocument.system.folder_id == folder.system.id)
.order_by(InvoiceDocument.system.id.ASC)
.execute()
)
Note the asymmetry: on the left of the comparison stands class access, which yields an ECMField, on
the right an instance value. A system field without a .system property is reachable through item
access, InvoiceDocument["SDSTA_ID"].
18. Child objects (with_children)
HOL only. select_lol() offers no equivalent, see Choosing a mode.
|
.with_children() switches to a HOL query that returns each main object together with its child objects. The call returns an ECMModelQueryHolSync. .limit() must be set before execute() or stream() because HOL responses do not support pagination.
Each result is an ECMHolResult with:
-
.main— the main object (e.g. the folder) -
.children_of(ECMChildSpec(ChildModel))— list of child objects of that type
-
Sync
-
Async
from ecmind_blue_client.ecm.model import ECMChildSpec
results = (
ecm.dms.select(InvoiceFolder)
.where(InvoiceFolder.Year == 2024)
.with_children(ECMChildSpec(InvoiceDocument))
.limit(100)
.execute()
)
for r in results:
print(f"Folder {r.main.Title}:")
for doc in r.children_of(ECMChildSpec(InvoiceDocument)):
print(f" Document {doc.system.id}")
from ecmind_blue_client.ecm.model import ECMChildSpec
results = await (
ecm.dms.select(InvoiceFolder)
.where(InvoiceFolder.Year == 2024)
.with_children(ECMChildSpec(InvoiceDocument))
.limit(100)
.execute()
)
for r in results:
print(f"Folder {r.main.Title}:")
for doc in r.children_of(ECMChildSpec(InvoiceDocument)):
print(f" Document {doc.system.id}")
19. Parent objects (with_parents)
HOL only. select_lol() offers no equivalent, see Choosing a mode.
|
.with_parents() inverts the response structure: the outermost parent type (first spec) becomes the main object result.main. The originally queried object and all intermediate levels are accessible via .children_of().
Specs are passed from outermost to innermost (e.g. Folder first, then Register).
-
Sync
-
Async
from ecmind_blue_client.ecm.model import ECMChildSpec, ECMParentSpec
results = (
ecm.dms.select(InvoiceDocument)
.where(InvoiceDocument.Status == "Approved")
.with_parents(ECMParentSpec(InvoiceFolder), ECMParentSpec(InvoiceRegister))
.limit(50)
.execute()
)
for r in results:
folder = r.main # InvoiceFolder
register = r.children_of(ECMChildSpec(InvoiceRegister))[0]
doc = r.children_of(ECMChildSpec(InvoiceDocument))[0]
print(f"{folder.Title} → {register.Title} → Doc {doc.system.id}")
from ecmind_blue_client.ecm.model import ECMChildSpec, ECMParentSpec
results = await (
ecm.dms.select(InvoiceDocument)
.where(InvoiceDocument.Status == "Approved")
.with_parents(ECMParentSpec(InvoiceFolder), ECMParentSpec(InvoiceRegister))
.limit(50)
.execute()
)
for r in results:
folder = r.main
register = r.children_of(ECMChildSpec(InvoiceRegister))[0]
doc = r.children_of(ECMChildSpec(InvoiceDocument))[0]
print(f"{folder.Title} → {register.Title} → Doc {doc.system.id}")
20. Examples
20.1. Basic query with filter and sorting
-
Sync
-
Async
from ecmind_blue_client.ecm.model import ECMFolderModel, ECMField
class InvoiceFolder(ECMFolderModel):
_internal_name_ = "InvoiceFolder"
Title: ECMField[str]
Year: ECMField[int]
for folder in (
ecm.dms.select(InvoiceFolder)
.where(InvoiceFolder.Year >= 2020)
.order_by(InvoiceFolder.Year.DESC)
.stream()
):
print(folder.system.id, folder.Title)
from ecmind_blue_client.ecm.model import ECMFolderModel, ECMField
class InvoiceFolder(ECMFolderModel):
_internal_name_ = "InvoiceFolder"
Title: ECMField[str]
Year: ECMField[int]
async for folder in (
ecm.dms.select(InvoiceFolder)
.where(InvoiceFolder.Year >= 2020)
.order_by(InvoiceFolder.Year.DESC)
.stream()
):
print(folder.system.id, folder.Title)
20.2. Combined conditions (AND / OR)
-
Sync
-
Async
for folder in (
ecm.dms.select(InvoiceFolder)
.where(
(InvoiceFolder.Year >= 2020) & (InvoiceFolder.Year <= 2024),
InvoiceFolder.Title == "Invoice",
)
.stream()
):
print(folder.Title, folder.Year)
async for folder in (
ecm.dms.select(InvoiceFolder)
.where(
(InvoiceFolder.Year >= 2020) & (InvoiceFolder.Year <= 2024),
InvoiceFolder.Title == "Invoice",
)
.stream()
):
print(folder.Title, folder.Year)
20.3. Limit, offset and page size
-
Sync
-
Async
# At most 50 results, starting at position 100, in batches of 25 per server request
for folder in (
ecm.dms.select(InvoiceFolder)
.limit(50)
.offset(100)
.pagesize(25)
.stream()
):
print(folder.system.id)
async for folder in (
ecm.dms.select(InvoiceFolder)
.limit(50)
.offset(100)
.pagesize(25)
.stream()
):
print(folder.system.id)
20.4. Rights and audit metadata
-
Sync
-
Async
for folder in (
ecm.dms.select(InvoiceFolder)
.rights()
.base_params()
.stream()
):
print(folder.system.rights)
print(folder.system.base_params)
async for folder in (
ecm.dms.select(InvoiceFolder)
.rights()
.base_params()
.stream()
):
print(folder.system.rights)
print(folder.system.base_params)
20.5. Selective fields
Use .fields() to load only the specified fields from the server. This significantly reduces
the amount of data transferred when only a few fields are needed.
-
Sync
-
Async
for folder in (
ecm.dms.select(InvoiceFolder)
.fields(InvoiceFolder.Title, InvoiceFolder.Year)
.stream()
):
print(folder.Title, folder.Year)
# folder.OtherField would be None (not loaded)
async for folder in (
ecm.dms.select(InvoiceFolder)
.fields(InvoiceFolder.Title, InvoiceFolder.Year)
.stream()
):
print(folder.Title, folder.Year)
20.6. Querying the recycle bin
-
Sync
-
Async
# Only deleted objects
for folder in ecm.dms.select(InvoiceFolder).garbage_mode().stream():
print(folder.system.id, folder.Title)
async for folder in ecm.dms.select(InvoiceFolder).garbage_mode().stream():
print(folder.system.id, folder.Title)
20.7. Generic model (without a class definition)
When the object type is only known at runtime, a model can be created dynamically:
-
Sync
-
Async
from ecmind_blue_client.ecm.model import make_folder_model
InvoiceFolder = make_folder_model("InvoiceFolder")
for folder in ecm.dms.select(InvoiceFolder).stream():
print(folder.system.id, folder["Title"])
from ecmind_blue_client.ecm.model import make_folder_model
InvoiceFolder = make_folder_model("InvoiceFolder")
async for folder in ecm.dms.select(InvoiceFolder).stream():
print(folder.system.id, folder["Title"])
20.8. Combined query across document, register and folder
HOL only. select_lol() offers no equivalent, see Choosing a mode.
|
-
Sync
-
Async
from tests.models.Unittest_DMS import Unittest_DMS, Unittest_DMS_Register, Unittest_DMS_Document
for doc in (
ecm.dms.select(Unittest_DMS_Document)
.where(
Unittest_DMS_Document.StringField == "Invoice",
Unittest_DMS_Register.Name == "Incoming invoices",
Unittest_DMS.Name == "Supplier GmbH",
)
.stream()
):
print(doc.system.id, doc.Name)
from tests.models.Unittest_DMS import Unittest_DMS, Unittest_DMS_Register, Unittest_DMS_Document
async for doc in (
ecm.dms.select(Unittest_DMS_Document)
.where(
Unittest_DMS_Document.StringField == "Invoice",
Unittest_DMS_Register.Name == "Incoming invoices",
Unittest_DMS.Name == "Supplier GmbH",
)
.stream()
):
print(doc.system.id, doc.Name)
20.9. Search for a folder containing a matching child object
HOL only. select_lol() offers no equivalent, see Choosing a mode.
|
-
Sync
-
Async
from tests.models.Unittest_DMS import Unittest_DMS, Unittest_DMS_Document
for folder in (
ecm.dms.select(Unittest_DMS)
.where(
Unittest_DMS.Name == "Supplier GmbH",
Unittest_DMS_Document.StringField == "Invoice",
)
.stream()
):
print(folder.system.id, folder.Name)
from tests.models.Unittest_DMS import Unittest_DMS, Unittest_DMS_Document
async for folder in (
ecm.dms.select(Unittest_DMS)
.where(
Unittest_DMS.Name == "Supplier GmbH",
Unittest_DMS_Document.StringField == "Invoice",
)
.stream()
):
print(folder.system.id, folder.Name)
20.10. A flat LOL query
-
Sync
-
Async
for folder in (
ecm.dms.select_lol(InvoiceFolder)
.where(InvoiceFolder.Year >= 2020)
.order_by(InvoiceFolder.Year.DESC)
.stream()
):
print(folder.system.id, folder.Title)
async for folder in (
ecm.dms.select_lol(InvoiceFolder)
.where(InvoiceFolder.Year >= 2020)
.order_by(InvoiceFolder.Year.DESC)
.stream()
):
print(folder.system.id, folder.Title)