= Getting started
:description: Connect, authenticate, and run your first ECM queries using SyncPoolClient or AsyncPoolClient. Requirements: Python >=3.12, UV, and TCP access to the enaio server.
:keywords: quickstart, installation, SyncPoolClient, AsyncPoolClient, SessionLogin, connection, enaio
:page-tags: guide

== Sync or Async?

The library offers two API variants that differ in how they execute requests:

[cols="1,1", options="header"]
|===
|Sync (`SyncPoolClient`)
|Async (`AsyncPoolClient`)

|Blocks the calling thread until the response is received.
|Yields control back to the event loop while waiting for the response.

|Simple, linear code without `async`/`await`.
|Requires `async def` functions and `await`.

|Suitable for scripts, batch processes, and importers.
|Suitable for web applications and other event-loop-based systems.
|===

*Sync* is a good fit wherever code runs sequentially and no concurrent processing is needed — for example in import scripts, migrations, or command-line tools.

*Async* is the right choice when the client is embedded in an existing event loop, such as https://fastapi.tiangolo.com/[FastAPI] endpoints.
In that context a blocking sync client would stall the entire event loop and delay all concurrent requests.

[tabs]
====
Sync::
+
[source,python]
----
# Import script: SyncPoolClient with make_folder_model
from ecmind_blue_client.ecm import ECM
from ecmind_blue_client.pool import SyncPoolClient
from ecmind_blue_client.ecm.model import make_folder_model

ecm = ECM(SyncPoolClient(servers="<host>:4000:1", username="<user>", password="<pass>"))
InvoiceFolder = make_folder_model("InvoiceFolder")

for folder in ecm.dms.select(InvoiceFolder).stream():
    print(folder.system.id, folder["Title"])
----

Async::
+
[source,python]
----
# FastAPI endpoint: AsyncPoolClient with make_folder_model
from fastapi import FastAPI
from ecmind_blue_client.ecm import ECM
from ecmind_blue_client.pool import AsyncPoolClient
from ecmind_blue_client.ecm.model import make_folder_model

app = FastAPI()
ecm = ECM(AsyncPoolClient(servers="<host>:4000:1", username="<user>", password="<pass>"))
InvoiceFolder = make_folder_model("InvoiceFolder")

@app.get("/folders")
async def list_folders():
    return [
        {"id": folder.system.id, "title": folder["Title"]}
        async for folder in ecm.dms.select(InvoiceFolder).stream()
    ]
----
====

== Installation

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

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

== Connecting

[tabs]
====
Sync::
+
[source,python]
----
from ecmind_blue_client.ecm import ECM
from ecmind_blue_client.pool import SyncPoolClient

client = SyncPoolClient(
    servers="<host>:4000:1",
    username="<username>",
    password="<password>",
)
ecm = ECM(client)
----

Async::
+
[source,python]
----
from ecmind_blue_client.ecm import ECM
from ecmind_blue_client.pool import AsyncPoolClient

client = AsyncPoolClient(
    servers="<host>:4000:1",
    username="<username>",
    password="<password>",
)
ecm = ECM(client)
----
====

The format for `servers` is `<host>:<port>:<weight>`. Multiple servers are separated by `#`.
The weight controls how connections are distributed across servers during load balancing.

[tabs]
====
Sync::
+
[source,python]
----
# Multiple servers with weights
client = SyncPoolClient(
    servers="<host1>:4000:2#<host2>:4000:1",
    username="<username>",
    password="<password>",
)
----

Async::
+
[source,python]
----
# Multiple servers with weights
client = AsyncPoolClient(
    servers="<host1>:4000:2#<host2>:4000:1",
    username="<username>",
    password="<password>",
)
----
====

== Querying objects

The API offers two methods for retrieving results:

`stream()`:: Reads results page by page from the server, yielding one object at a time.
Recommended for large result sets, as it never holds all objects in memory at once.
`execute()`:: Fetches all results upfront and returns a list.
Convenient for small result sets or when the total count is needed before processing.

[CAUTION]
====
`execute()` loads all objects into memory before processing begins.
With large result sets this can cause memory problems.

`stream()` works page by page — but paging in enaio is not transactional.
If objects are modified while iterating, pages may contain inconsistent state or objects may
appear twice. In that case `execute()` should be preferred.
====

=== Generic approach

Without a model definition, objects can be queried directly using the internal name of the object type:

[tabs]
====
Sync::
+
[source,python]
----
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"])
----

Async::
+
[source,python]
----
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"])
----
====

=== With a typed model

For IDE code completion and static type checking, defining a model class is recommended.
Any changes to the object definition on the server become immediately visible via the type checker.

[tabs]
====
Sync::
+
[source,python]
----
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 >= 2024)
    .order_by(InvoiceFolder.Year.DESC)
    .stream()
):
    print(folder.system.id, folder.Title)
----

Async::
+
[source,python]
----
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 >= 2024)
    .order_by(InvoiceFolder.Year.DESC)
    .stream()
):
    print(folder.system.id, folder.Title)
----
====

Models can also be generated automatically from a running ECM instance — see xref:guides/generate-models.adoc[ecm-generate-models].

== Inserting objects

[tabs]
====
Sync::
+
[source,python]
----
folder = ecm.dms.insert_and_get(InvoiceFolder(Title="Invoice 2024", Year=2024))
print(folder.system.id)
----

Async::
+
[source,python]
----
folder = await ecm.dms.insert_and_get(InvoiceFolder(Title="Invoice 2024", Year=2024))
print(folder.system.id)
----
====

== SQL queries

[tabs]
====
Sync::
+
[source,python]
----
result = ecm.db.select(
    "SELECT id, benutzer FROM benutzer WHERE benutzer = %s",
    "admin",
)
for row in result:
    print(row["benutzer"])
----

Async::
+
[source,python]
----
result = await ecm.db.select(
    "SELECT id, benutzer FROM benutzer WHERE benutzer = %s",
    "admin",
)
for row in result:
    print(row["benutzer"])
----
====

== Impersonation

To perform actions on behalf of another user, use `impersonate()`.
The executing user requires the *Context Switch* system role.

[tabs]
====
Sync::
+
[source,python]
----
# As a context manager
with ecm.impersonate("john") as ecm_john:
    folder = ecm_john.dms.insert_and_get(InvoiceFolder(Title="Test"))

# Or directly without a with block
ecm_john = ecm.impersonate("john")
folder = ecm_john.dms.insert_and_get(InvoiceFolder(Title="Test"))
----

Async::
+
[source,python]
----
# As a context manager
async with ecm.impersonate("john") as ecm_john:
    folder = await ecm_john.dms.insert_and_get(InvoiceFolder(Title="Test"))

# Or directly without a with block
ecm_john = ecm.impersonate("john")
folder = await ecm_john.dms.insert_and_get(InvoiceFolder(Title="Test"))
----
====
