Ecmind_blue_client: Release Notes 1.0.0

ecmind_blue_client 1.0.0

In 0.9.2 verteilt sich die Funktionalität auf zwei Ebenen. Am Client selbst hängen lol_query() für Abfragen, xml_import() zum Schreiben, dazu get_object_details(), store_in_cache(), execute_sql() und für alles Übrige execute(Job(...)). Alles Weitere kommt aus separaten Paketen, die über die Extras manage, objdef, portfolio und workflow dazuinstalliert werden, also Benutzerverwaltung, Objektdefinition, Portfolios und Workflow. Objektdaten kommen dabei als Dictionary mit internen Feldnamen zurück, Objekttypen und Felder werden als String angegeben.

In 1.0.0 liegen diese Ebenen zusammen in der typisierten Schicht ecmind_blue_client.ecm, gegliedert nach Namespaces und vollständig in einer synchronen und einer asynchronen Variante. Der Direktaufruf beliebiger Serverjobs bleibt dabei erhalten, er heisst jetzt ecm.execute() und ist weiter unten beschrieben. Der alte Client bleibt für einige Zeit bestehen.

Dokumentation inklusive Migrationsleitfaden (Deutsch und Englisch, pro Release versioniert): https://ecmind-blue-client.docs.ecmind.ch

Einstiegspunkt

Statt TcpPoolClient mit Connection-String werden SyncPoolClient beziehungsweise AsyncPoolClient verwendet und in ECM() gehängt:

from ecmind_blue_client.pool import SyncPoolClient, ServerConnectionSettings
from ecmind_blue_client.ecm import ECM

client = SyncPoolClient(servers="host:4000:1", username="...", password="...", name="MyApp")
ecm = ECM(client)

Der kompakte Connection-String im Format host:port:gewicht bleibt erhalten, alternativ gibt es ServerConnectionSettings. Ab hier laufen alle Operationen über Namespaces: ecm.dms (Ordner, Register, Dokumente), ecm.security (Benutzer, Gruppen, Rollen), ecm.system (Serverinfos, Definition, Lizenzen, Skripte), ecm.workflow und ecm.db.

Von Dictionaries zu typisierten Modellen

In 0.9.2 wird ein Objekttyp über seinen internen Namen adressiert und das Ergebnis über String-Schlüssel gelesen (dossier["OBJECT_ID"], dossier["LastName"]). In 1.0.0 gibt es dafür Modellklassen für Ordner, Register und Dokumente, mit Tabellenfeldern, Katalogfeldern als Enums, Pflichtfeldprüfung und Read-only-Feldern (always, init, arch).

Die Klassen müssen nicht von Hand geschrieben werden: ecm-generate-models erzeugt sie aus einem Live-Server oder aus einer asobjdef.xml. Feldnamen und Datentypen sind dadurch statisch bekannt, eine Umbenennung auf dem Server wird nach dem Neugenerieren vom Typechecker an allen betroffenen Stellen gemeldet. Wenn der Objekttyp erst zur Laufzeit feststeht, liefern make_folder_model, make_register_model, make_document_model und ecm.dms.model_by_name() das Modell dynamisch.

Abfragen: lol_query() wird zum Query Builder

Aus client.lol_query(object_name=..., result_fields=[...]) wird ecm.dms.select_lol(Model) beziehungsweise ecm.dms.select(Model) für HOL, jeweils mit Chain-Methoden statt Parameterlisten:

  • .where(...), .order_by(...), .limit(), .offset(), .fields(...), .execute() und .stream().
  • Bedingungen auf Tabellenfeldspalten, optional auf eine Zeile beschränkt (Invoice.Positions[3].Quantity == 5). Diese Variante war in lol_query nicht ausdrückbar.
  • Volltextbedingung über .fulltext(...).
  • Kombinierte Abfragen über Objekthierarchien, Eltern- und Kindobjekte kommen in einer Anfrage mit.
  • Systemfelder sind auf Klassenebene abfragbar (Model.system.id, Ersteller, Erstelldatum, Ablageort), inklusive korrektem system="1", was Sortierung und Keyset-Pagination über die Objekt-ID ermöglicht.
  • Suchflags und Archivstatus als boolesche Properties (system.in_register, system.archived, system.locked).

Schreiben: xml_import() wird aufgeteilt

Das bisherige xml_import() mit search_fields und action-Parametern ist in klar getrennte Operationen aufgelöst: insert(), update(), upsert() sowie die Varianten insert_and_get() / update_and_get(), die das erzeugte oder geänderte Objekt direkt typisiert zurückgeben. Dazu kommen delete(), move(), copy(), Dokumentvarianten sowie checkout(), undo_checkout() und checkin(). get_object_details() entspricht ecm.dms.get(), store_in_cache() entspricht ecm.dms.files(), execute_sql() entspricht ecm.db.select().

Zwei Verhaltensänderungen sind beim Schreiben relevant:

  • Partielles Update: Es werden nur geänderte Felder gesendet, nicht mehr alle geladenen. Tabellenfelder werden je nach Änderung angehängt oder ersetzt, steuerbar über replace_table_fields. In der alten Implementierung wurden Tabellenzeilen im Append-Modus dabei dupliziert.
  • Beschreibbare Systemfelder werden nicht mehr pauschal herausgefiltert, sondern über die Allowlist WRITABLE_SYSTEM_FIELDS zugelassen (OBJECT_FOREIGNID, OBJECT_SYSTEMID, OBJECT_USERGUID).

Ehemalige Zusatzpakete sind eingebaut

Was in 0.9.2 über die Extras manage, objdef und workflow und die zugehörigen Pakete lief, steckt jetzt direkt in den Namespaces:

  • Benutzer-, Gruppen- und Rollenverwaltung in ecm.security, inklusive Anlegen, Ändern, Löschen und Leeren von Gruppen sowie Export des Sicherheitssystems.
  • Objektdefinition typisiert über ecm.system.definition(), dazu ecm.system.jobs() (alle Serverjobs je Namespace), get_icons(), run_script(), get_user_data() und set_user_data(), Lizenz- und Modulabfragen.
  • Serverseitige Skript-Events verwalten (get_os_events(), create_os_event(), update_os_event(), delete_os_event()) und ohne Serverneustart neu laden (refresh_server_events()). Skriptcode wird als UTF-8 mit BOM übertragen, damit der volle Unicode-Umfang verlustfrei zurückkommt.
  • Workflow in ecm.workflow: Prozesse starten (start_process()), Workflow-Discovery, Postkorb-Auswertung (process_list_by_user()), Organisation, Vertretungen, Abwesenheiten. Workflow-Variablen gibt es typisiert, zur Laufzeit über workflow_model_by_name() oder statisch über ecm-generate-workflow-models.
  • Der Kontextwechsel heisst jetzt ecm.impersonate() und akzeptiert Benutzername, GUID oder numerische ID.

Ausgenommen ist portfolio: Diese Funktionalität ist bisher nicht in die neue API portiert worden.

Der direkte Job-Aufruf bleibt

Der bekannte Weg über rohe Serverjobs ist nicht verschwunden, er hat nur einen anderen Einstiegspunkt. Jobs ohne Entsprechung in einem Namespace werden weiterhin direkt aufgerufen, die verfügbaren Jobs stehen im Jobs-Enum statt als frei getippter String:

from ecmind_blue_client.rpc import Jobs

result = ecm.execute(Jobs.KRN_GETSERVERINFO, Flags=0, Info=6)

Verbindungshandling, Fehlerbehandlung und Impersonation verhalten sich dabei wie bei den typisierten Methoden. Für Jobs mit Dateirückgabe gibt es ecm.execute_streaming(). Der Funktionsumfang ist damit nicht auf den Ausbaustand der typisierten Schicht begrenzt.

Async in Web-Services wie FastAPI

Die asynchrone Variante ist kein Nebenschauplatz, sie deckt denselben Funktionsumfang ab wie die synchrone. In einem Event-Loop-Kontext wie FastAPI ist das der passende Weg: Der AsyncPoolClient wird einmal beim Start aufgebaut und über die Laufzeit wiederverwendet, gleichzeitige Anfragen teilen sich den Pool, und ein DMS-Aufruf blockiert den Event-Loop nicht. In 0.9.2 musste ein solcher Service die synchronen Client-Aufrufe in einen Threadpool auslagern.

from contextlib import asynccontextmanager
from fastapi import Depends, FastAPI, Request

from ecmind_blue_client.ecm import ECM, ECMAsync
from ecmind_blue_client.pool import AsyncPoolClient


@asynccontextmanager
async def lifespan(app: FastAPI):
    client = AsyncPoolClient(servers="host:4000:1", username="...", password="...")
    app.state.ecm = ECM(client)  # -> ECMAsync
    yield
    app.state.ecm = None


app = FastAPI(lifespan=lifespan)


def get_ecm(request: Request) -> ECMAsync:
    return request.app.state.ecm


@app.get("/documents")
async def list_documents(ecm: ECMAsync = Depends(get_ecm)):
    results = await ecm.dms.select(InvoiceDocument).limit(50).execute()
    return [{"id": doc.system.id, "number": doc.InvoiceNumber} for doc in results]

Die Dokumentation enthält dazu einen vollständigen Leitfaden mit Upsert, Paging, Detailabfrage und Download, inklusive Streaming der Dateiinhalte direkt in die HTTP-Antwort.

Verbindung, Pool und Sicherheit

  • RPC- und Pool-Schicht neu implementiert: eigenes pool-Paket, getrennte Verbindungs-, Job- und Fehlerobjekte, spezifische Exception-Typen statt generischer Fehler.
  • Verbindungspool über mehrere Server mit Gewichtung wie bisher, TLS ist jetzt Standard.
  • Eine gemeinsame Session pro Server: Folgeverbindungen hängen sich an die bestehende Session an, statt sich erneut anzumelden. Das reduziert die Anzahl Logins und Sessions gegenüber 0.9.2 deutlich.
  • AES-Passwortschema, wenn der Server Security\PwdDecryption > 1 meldet. Damit entfällt das Limit von 62 ASCII-Zeichen, Unicode-Passwörter bis 128 Zeichen sind möglich. AES-128 ist in reinem Python implementiert, die Bibliothek bleibt ohne Laufzeitabhängigkeiten.
  • ecm.check_connections() prüft jeden konfigurierten Server einzeln auf Erreichbarkeit, Anmeldung und Serverinfo.

Neu hinzugekommen

  • Streaming grosser Dokumente: ecm.dms.files_streaming() liest Dateianhänge in Chunks direkt von der Verbindung, ohne sie vollständig in den RAM oder in eine temporäre Datei zu schreiben. Gemessen an einem 3-GiB-Dokument: rund 37 MiB Spitzenspeicher. document_stream() liest zudem über die 2-GiB-Grenze hinaus.
  • Server-Push über einen eigenen Callback-Kanal: ecm.notification.job_calls(...) überwacht Serverjob-Aufrufe, ecm.notification.messages(...) empfängt Client-Nachrichten, ecm.notification.send_message(...) sendet sie, optional an alle konfigurierten Server. Für Diagnosezwecke gibt es ecm-callback-listen. Zu beachten: Die Job-Überwachung ist eine administrative Funktion und liefert die Parameter der überwachten Jobs unverändert mit.

Dokumentation

Die Dokumentation liegt nicht mehr im GitLab-Wiki, sondern als Antora-Site unter https://ecmind-blue-client.docs.ecmind.ch. Sie ist zweisprachig (Deutsch als Startseite, Englisch), pro Release versioniert und enthält neben der API-Referenz einen Migrationsleitfaden, der die alten Methoden den neuen gegenüberstellt.

Was entfällt

  • Die Extras manage, objdef und workflow entfallen, die Funktionalität ist in den Namespaces enthalten.
  • Das Extra portfolio entfällt ebenfalls, hier steht die Portierung in die neue API aber noch aus.
  • TcpClient und TcpPoolClient sowie die Extras tcp und TcpClient sind nur noch als Übergangslösung vorhanden und abgekündigt. Nachfolger sind SyncPoolClient und AsyncPoolClient zusammen mit ECM().
  • Die alte Job-und-Parameter-API rund um Client.execute(Job(...)) bleibt für die Migration bestehen und wird nicht weiterentwickelt. Der Ersatz für den Direktaufruf ist ecm.execute().
  • ecmind_blue_client.com_client und ecmind_blue_client.soap_client stehen nicht mehr zur Verfügung, sie waren bereits vor 1.0.0 abgekündigt und entfernt worden.
  • Mindestversion ist Python 3.12 statt 3.11. Die Bibliothek selbst hat weiterhin keine Laufzeitabhängigkeiten, XmlElement wird nur noch für den abgekündigten TCP-Pfad benötigt.
2 „Gefällt mir“

Hi @uw ,

vielen Dank für die neue Version.

Ich stelle zur Sicherheit eine konkrete Nachfrage. Wenn ich in einem Bestandsprojekt von 0.9.2 auf 1.0.0 update, was wäre dabei alles zu beachten?

Ist ein Update auf 1.0.0 inkrementell möglich - konkret: Stehen die Legacy Befehle ohne Code-Anpassung auch unter 1.0.0 zur Verfügung?

Hoi @danielstraub, ja, die Idee ist, dass man einfach 1.0 nehmen kann und alles über die Legacy-APIs weiterläuft.

Wir haben dies mit diversen Projekten getestet; Ihr müsst aber natürlich grosse Sorgfalt beim Testen walten lassen, da unter anderem auch die Basis umgestellt wurde (weniger Abhängigkeiten, direktere Erstellung des Binary-Protokolls).

Ich meinte, Ihr müsstet wenigstens dafür Sorge tragen, dass nicht mehr ecmind-blue-client[TcpClient] sondern der neuere Syntax ecmind-blue-client[tcp] importiert wird. Abgesehen davon sind mir keine Probleme (mehr) aufgefallen. (Wenn Du ein Programm komplett umgestellt hättest, wäre es dann nur noch ecmind-blue-client.)