docuware_search.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. """
  2. DocuWare: gueltige Suchfilter ermitteln und Dokumente abfragen.
  3. Voraussetzungen:
  4. pip install requests
  5. Konfiguration per Umgebungsvariablen:
  6. DOCUWARE_SERVER_URL z.B. https://your-system.docuware.cloud
  7. DOCUWARE_USERNAME DocuWare Benutzername
  8. DOCUWARE_PASSWORD DocuWare Passwort
  9. DOCUWARE_FILE_CABINET optional: Name des File Cabinets
  10. DOCUWARE_ORG_ID optional: OrgId, falls bei On-Prem/Enterprise nötig
  11. Beispiele:
  12. python docuware_search.py --list-filters
  13. python docuware_search.py --list-documents --count 10 --fields DOCUMENT_TYPE,COMPANY_NAME
  14. python docuware_search.py --search DOCUMENT_TYPE "Invoice" --count 10
  15. python docuware_search.py --search DWSTOREDATETIME 2024-01-01 2024-12-31 --count 20
  16. """
  17. import argparse
  18. import json
  19. import os
  20. import sys
  21. from typing import Any
  22. import requests
  23. from dotenv import load_dotenv
  24. load_dotenv()
  25. PLATFORM = "DocuWare/Platform"
  26. CLIENT_ID = "docuware.platform.net.client"
  27. SCOPE = "docuware.platform"
  28. class DocuWareClient:
  29. def __init__(self, server_url: str, username: str, password: str, org_id: str | None = None) -> None:
  30. self.server_url = server_url.rstrip("/")
  31. self.username = username
  32. self.password = password
  33. self.org_id = org_id
  34. self.session = requests.Session()
  35. self.session.headers.update({"Accept": "application/json"})
  36. self.access_token: str | None = None
  37. def _url(self, path: str) -> str:
  38. return f"{self.server_url}/{PLATFORM}/{path.lstrip('/')}"
  39. def _request(self, method: str, url: str, **kwargs: Any) -> requests.Response:
  40. response = self.session.request(method, url, timeout=60, **kwargs)
  41. try:
  42. response.raise_for_status()
  43. except requests.HTTPError as exc:
  44. body = response.text[:2000]
  45. raise RuntimeError(f"{method} {url} fehlgeschlagen: HTTP {response.status_code}\n{body}") from exc
  46. return response
  47. def authenticate_password_grant(self) -> None:
  48. """
  49. Entspricht in der Postman-Collection:
  50. 1. Home/IdentityServiceInfo
  51. 2. /.well-known/openid-configuration
  52. 3.a Request Token w/ Username & Password
  53. """
  54. identity_info = self._request(
  55. "GET",
  56. self._url("Home/IdentityServiceInfo"),
  57. ).json()
  58. identity_service_url = identity_info["IdentityServiceUrl"].rstrip("/")
  59. openid_config = self._request(
  60. "GET",
  61. f"{identity_service_url}/.well-known/openid-configuration",
  62. ).json()
  63. token_endpoint = openid_config["token_endpoint"]
  64. token_response = self._request(
  65. "POST",
  66. token_endpoint,
  67. headers={"Accept": "application/json"},
  68. data={
  69. "grant_type": "password",
  70. "scope": SCOPE,
  71. "client_id": CLIENT_ID,
  72. "username": self.username,
  73. "password": self.password,
  74. },
  75. ).json()
  76. self.access_token = token_response["access_token"]
  77. self.session.headers.update({"Authorization": f"Bearer {self.access_token}"})
  78. def get_file_cabinets(self) -> list[dict[str, Any]]:
  79. params = {}
  80. if self.org_id:
  81. params["OrgId"] = self.org_id
  82. data = self._request("GET", self._url("FileCabinets"), params=params).json()
  83. return data.get("FileCabinet", [])
  84. def select_file_cabinet(self, file_cabinet_name: str | None = None) -> dict[str, Any]:
  85. cabinets = self.get_file_cabinets()
  86. if not cabinets:
  87. raise RuntimeError("Keine File Cabinets gefunden oder keine Berechtigung.")
  88. if file_cabinet_name:
  89. for cabinet in cabinets:
  90. if cabinet.get("Name") == file_cabinet_name:
  91. return cabinet
  92. names = ", ".join(c.get("Name", "<ohne Name>") for c in cabinets)
  93. raise RuntimeError(f"File Cabinet '{file_cabinet_name}' nicht gefunden. Verfuegbar: {names}")
  94. return cabinets[0]
  95. def get_dialogs(self, file_cabinet_id: str, dialog_type: str | None = None) -> list[dict[str, Any]]:
  96. params = {}
  97. if dialog_type:
  98. params["DialogType"] = dialog_type
  99. data = self._request(
  100. "GET",
  101. self._url(f"FileCabinets/{file_cabinet_id}/Dialogs"),
  102. params=params,
  103. ).json()
  104. return data.get("Dialog", [])
  105. def select_search_dialog(self, file_cabinet_id: str) -> dict[str, Any]:
  106. dialogs = self.get_dialogs(file_cabinet_id, dialog_type="Search")
  107. if not dialogs:
  108. # Fallback: alle Dialoge laden und nach Type suchen.
  109. dialogs = [d for d in self.get_dialogs(file_cabinet_id) if d.get("Type") == "Search"]
  110. if not dialogs:
  111. raise RuntimeError("Kein Search-Dialog gefunden.")
  112. for dialog in dialogs:
  113. if dialog.get("IsDefault") is True:
  114. return dialog
  115. return dialogs[0]
  116. def get_dialog_details(self, file_cabinet_id: str, search_dialog_id: str) -> dict[str, Any]:
  117. return self._request(
  118. "GET",
  119. self._url(f"FileCabinets/{file_cabinet_id}/Dialogs/{search_dialog_id}"),
  120. ).json()
  121. def get_valid_search_filters(self, file_cabinet_id: str, search_dialog_id: str) -> list[dict[str, Any]]:
  122. """
  123. Die gueltigen Suchfilter sind die Felder des Search-Dialogs.
  124. Fuer Abfragen wird typischerweise DBFieldName als Condition.DBName verwendet.
  125. """
  126. details = self.get_dialog_details(file_cabinet_id, search_dialog_id)
  127. fields = details.get("Fields", [])
  128. result = []
  129. for field in fields:
  130. if field.get("Visible", True):
  131. result.append(
  132. {
  133. "db_name": field.get("DBFieldName"),
  134. "label": field.get("DlgLabel"),
  135. "type": field.get("DWFieldType"),
  136. "read_only": field.get("ReadOnly"),
  137. "not_empty": field.get("NotEmpty"),
  138. "allow_extended_search": field.get("AllowExtendedSearch"),
  139. "length": field.get("Length"),
  140. "precision": field.get("Precision"),
  141. }
  142. )
  143. return result
  144. def list_documents(
  145. self,
  146. file_cabinet_id: str,
  147. count: int = 10,
  148. fields: list[str] | None = None,
  149. ) -> dict[str, Any]:
  150. """
  151. Einfache Dokumentliste ohne Suchbedingung.
  152. """
  153. params: dict[str, Any] = {"Count": count}
  154. if fields:
  155. params["Fields"] = ",".join(fields)
  156. return self._request(
  157. "GET",
  158. self._url(f"FileCabinets/{file_cabinet_id}/Documents"),
  159. params=params,
  160. ).json()
  161. def search_documents(
  162. self,
  163. file_cabinet_id: str,
  164. search_dialog_id: str,
  165. conditions: list[dict[str, Any]],
  166. operation: str = "And",
  167. count: int = 10,
  168. start: int = 0,
  169. result_fields: list[str] | None = None,
  170. sort_field: str | None = None,
  171. sort_direction: str = "Asc",
  172. ) -> dict[str, Any]:
  173. """
  174. Suche per DialogExpression.
  175. conditions Beispiel:
  176. [{"DBName": "DOCUMENT_TYPE", "Value": ["Invoice"]}]
  177. [{"DBName": "DWSTOREDATETIME", "Value": ["2024-01-01", "2024-12-31"]}]
  178. Mehrere Werte innerhalb einer Condition werden von DocuWare als OR interpretiert.
  179. Mehrere Conditions werden ueber Operation ("And" / "Or") kombiniert.
  180. """
  181. body: dict[str, Any] = {
  182. "Condition": conditions,
  183. "Operation": operation,
  184. "Start": start,
  185. "Count": count,
  186. "ForceRefresh": True,
  187. "IncludeSuggestions": False,
  188. }
  189. if result_fields:
  190. body["AdditionalResultFields"] = result_fields
  191. if sort_field:
  192. body["SortOrder"] = [{"Field": sort_field, "Direction": sort_direction}]
  193. params = {"DialogId": search_dialog_id}
  194. return self._request(
  195. "POST",
  196. self._url(f"FileCabinets/{file_cabinet_id}/Query/DialogExpression"),
  197. params=params,
  198. json=body,
  199. ).json()
  200. def print_json(data: Any) -> None:
  201. print(json.dumps(data, indent=2, ensure_ascii=False))
  202. def main() -> int:
  203. parser = argparse.ArgumentParser(description="DocuWare Suchfilter und Dokumente per requests abfragen")
  204. parser.add_argument(
  205. "--file-cabinet", default=os.environ.get("DOCUWARE_FILE_CABINET"), help="Name des File Cabinets"
  206. )
  207. parser.add_argument("--count", type=int, default=10, help="Maximale Anzahl Dokumente")
  208. parser.add_argument("--fields", default="", help="Kommagetrennte Ergebnisfelder, z.B. DOCUMENT_TYPE,COMPANY_NAME")
  209. parser.add_argument("--list-filters", action="store_true", help="Gueltige Suchfelder des Search-Dialogs ausgeben")
  210. parser.add_argument("--list-documents", action="store_true", help="Dokumente ohne Suchbedingung listen")
  211. parser.add_argument(
  212. "--search",
  213. nargs="+",
  214. metavar=("DB_FIELD", "VALUE"),
  215. help="Suche: DB-Feld plus ein oder mehrere Werte, z.B. --search DOCUMENT_TYPE Invoice",
  216. )
  217. parser.add_argument("--operation", choices=["And", "Or"], default="And", help="Verknuepfung mehrerer Conditions")
  218. parser.add_argument("--sort-field", help="DB-Feld zum Sortieren")
  219. parser.add_argument("--sort-direction", choices=["Asc", "Desc"], default="Asc")
  220. args = parser.parse_args()
  221. server_url = os.environ.get("DOCUWARE_SERVER_URL")
  222. username = os.environ.get("DOCUWARE_USERNAME")
  223. password = os.environ.get("DOCUWARE_PASSWORD")
  224. org_id = os.environ.get("DOCUWARE_ORG_ID")
  225. missing = [
  226. name
  227. for name, value in {
  228. "DOCUWARE_SERVER_URL": server_url,
  229. "DOCUWARE_USERNAME": username,
  230. "DOCUWARE_PASSWORD": password,
  231. }.items()
  232. if not value
  233. ]
  234. if missing:
  235. print(f"Fehlende Umgebungsvariablen: {', '.join(missing)}", file=sys.stderr)
  236. return 2
  237. client = DocuWareClient(server_url=server_url, username=username, password=password, org_id=org_id)
  238. client.authenticate_password_grant()
  239. cabinet = client.select_file_cabinet(args.file_cabinet)
  240. file_cabinet_id = cabinet["Id"]
  241. search_dialog = client.select_search_dialog(file_cabinet_id)
  242. search_dialog_id = search_dialog["Id"]
  243. field_list = [f.strip() for f in args.fields.split(",") if f.strip()]
  244. if args.list_filters:
  245. filters = client.get_valid_search_filters(file_cabinet_id, search_dialog_id)
  246. print_json(
  247. {
  248. "file_cabinet": {"id": file_cabinet_id, "name": cabinet.get("Name")},
  249. "search_dialog": {"id": search_dialog_id, "name": search_dialog.get("DisplayName")},
  250. "valid_filters": filters,
  251. }
  252. )
  253. return 0
  254. if args.list_documents:
  255. print_json(client.list_documents(file_cabinet_id, count=args.count, fields=field_list or None))
  256. return 0
  257. if args.search:
  258. if len(args.search) < 2:
  259. print("--search erwartet DB_FIELD und mindestens einen VALUE", file=sys.stderr)
  260. return 2
  261. db_field = args.search[0]
  262. values = args.search[1:]
  263. conditions = [{"DBName": db_field, "Value": values}]
  264. print_json(
  265. client.search_documents(
  266. file_cabinet_id=file_cabinet_id,
  267. search_dialog_id=search_dialog_id,
  268. conditions=conditions,
  269. operation=args.operation,
  270. count=args.count,
  271. result_fields=field_list or None,
  272. sort_field=args.sort_field,
  273. sort_direction=args.sort_direction,
  274. )
  275. )
  276. return 0
  277. parser.print_help()
  278. return 0
  279. if __name__ == "__main__":
  280. raise SystemExit(main())