From f5a4342bcfa272d42f0712be4c3f79456619581a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20T=C3=BCrk?= Date: Mon, 13 Nov 2023 12:36:21 +0100 Subject: [PATCH] =?UTF-8?q?Verbindungsaufbau=20nur=20wenn=20n=C3=B6tig?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/source/abhaengigkeiten.rst | 6 + docs/source/anwendungen.rst | 11 +- docs/source/conf.py | 4 +- docs/source/examples.rst | 41 ++++++- examples/applus_configs.py | 18 ++- examples/complete_sql.pyw | 105 +++++++++++++++++ examples/importViewUDF.py | 75 ++++++++++++ examples/importViewUDFDeploy.pyw | 15 +++ examples/importViewUDFTest.pyw | 15 +++ examples/read_settings.py | 4 +- pyproject.toml | 7 +- src/PyAPplus64/applus.py | 175 ++++++++++++++++++++++++---- src/PyAPplus64/applus_job.py | 10 +- src/PyAPplus64/applus_scripttool.py | 25 +++- src/PyAPplus64/applus_server.py | 4 +- src/PyAPplus64/applus_sysconf.py | 11 +- src/PyAPplus64/sql_utils.py | 3 +- 17 files changed, 490 insertions(+), 39 deletions(-) create mode 100644 examples/complete_sql.pyw create mode 100644 examples/importViewUDF.py create mode 100644 examples/importViewUDFDeploy.pyw create mode 100644 examples/importViewUDFTest.pyw diff --git a/docs/source/abhaengigkeiten.rst b/docs/source/abhaengigkeiten.rst index 9db01bb..179a19b 100644 --- a/docs/source/abhaengigkeiten.rst +++ b/docs/source/abhaengigkeiten.rst @@ -46,3 +46,9 @@ Pandas / SqlAlchemy / xlsxwriter Sollen Excel-Dateien mit Pandas erzeugt, werden, so muss Pandas, SqlAlchemy und xlsxwriter installiert sein (`python -m pip install pandas sqlalchemy xlsxwriter`). + +PySimpleGUI und andere +---------------------- +Einige Beispiele benutzen PySimpleGUI (``python -m pip install pysimplegui``) +sowie teilweise spezielle Bibliotheken etwa zum Pretty-Printing von SQL (``python -m pip install sqlparse sqlfmt``). Dies +sind aber Abhängigkeiten von Beispielen, nicht der Bibliothek selbst. diff --git a/docs/source/anwendungen.rst b/docs/source/anwendungen.rst index 747f779..6902faf 100644 --- a/docs/source/anwendungen.rst +++ b/docs/source/anwendungen.rst @@ -4,10 +4,11 @@ typische Anwendungsfälle einfache Admin-Aufgaben ----------------------- -Selten auftretende Admin-Aufgaben lassen sich gut mittels Python-Scripten automatisieren. -Es ist sehr einfach möglich, auf die DB, aber auch auf SOAP-Schnittstelle zuzugreifen. -Ich habe dies vor allem für Wartungsarbeiten an Anpassungstabellen, die für eigene Erweiterungen -entwickelt wurden, genutzt. +Selten auftretende Admin-Aufgaben lassen sich gut mittels Python-Scripten +automatisieren. Es ist sehr einfach möglich, auf die DB, aber auch auf +SOAP-Schnittstelle der APP-Serverse zuzugreifen. Zudem ist rudimentärer Zugriff +auf ASMX-Seiten implementiert. Ich habe dies vor allem für Wartungsarbeiten an +Anpassungstabellen genutzt, die für eigene Erweiterungen entwickelt wurden. Als triviales Beispiel sucht folgender Code alle `DOCUMENTS` Einträge in Artikeln (angezeigt als `Bild` in `ArtikelRec.aspx`), für die Datei, auf die @@ -88,7 +89,7 @@ Dank der Bibliothek `zeep` ist es auch sehr einfach möglich, auf beliebige SOAP Beispielsweise kann auf die Sys-Config auch händisch, d.h. durch direkten Aufruf einer SOAP-Methode des APP-Servers zugegriffen werden:: - client = server.server_conn.getAppClient("p2system", "SysConf"); + client = server.getAppClient("p2system", "SysConf"); print (client.service.getString("STAMM", "MYLAND")) diff --git a/docs/source/conf.py b/docs/source/conf.py index 0fe40b9..b6958a8 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -14,8 +14,8 @@ sys.path.append('../src/') project = 'PyAPplus64' copyright = '2023, Thomas Tuerk' author = 'Thomas Tuerk' -version = '1.1.1' -release = '1.1.1' +version = '1.1.2' +release = '1.1.2' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/docs/source/examples.rst b/docs/source/examples.rst index f889c4b..e13456b 100644 --- a/docs/source/examples.rst +++ b/docs/source/examples.rst @@ -18,7 +18,8 @@ das Deploy-, das Test- und das Prod-System. Ein Beispiel ist im Unterverzeichnis Damit nicht in jedem Script immer wieder neu die Konfig-Dateien ausgewählt werden müssen, werden die Konfigs für das Prod-, Test- und Deploy-System in ``examples/applus_configs.py`` hinterlegt. Diese Datei wird in allen Scripten importiert, -so dass das Config-Verzeichnis und die darin enthaltenen Configs einfach zur Verfügung stehen. +so dass das Config-Verzeichnis und die darin enthaltenen Configs einfach zur Verfügung stehen. Zudem werden in dieser Datei auch alle verwendeten +Kombinationen aus System und Umgebung hinterlegt. So kann in Scripten auch eine Auswahl des Systems implementiert werden. .. literalinclude:: ../../examples/applus_configs.py :language: python @@ -74,6 +75,44 @@ Die GUI wird um die Erzeugung von Excel-Dateien mit Mengenabweichungen gebaut. :lines: 9- :linenos: +``complete_sql.pyw`` +-------------------- +Beispiel, wie ein einfacher APP-Server Aufruf über eine GUI zur Verfügung gestellt und mittels +Python-Bibliotheken erweitert werden kann. Zudem wird demonstriert, wie eine Auswahl verschiedenere +Systeme und Umgebungen realisiert werden kann. + +.. literalinclude:: ../../examples/complete_sql.pyw + :language: python + :lines: 9- + :linenos: + +``importViewUDF.py`` +-------------------- +Folgende Scripte erlauben den einfachen Import von DB-Anpass-Dateien, Views und UDFs über den Windows-Explorer. +Werden Verknüpfungen zu den Scripten ``importViewUDFDeploy.pyw`` und ``importViewUDFTest.pyw`` in ``%appdata%\Microsoft\Windows\SendTo`` abgelegt, +so können eine oder mehrerer solcher Dateien mittels _Kontextmenü (Rechtsklick) - Senden an_ an APplus zur Verarbeitung übergeben werden. +Dabei ist es wichtig, dass sich die Dateien im für den jeweiligen Typ passenden Verzeichnis befinden. + +.. literalinclude:: ../../examples/importViewUDF.py + :language: python + :lines: 9- + :linenos: + +Wrapper für Deploy-System: + +.. literalinclude:: ../../examples/importViewUDFDeploy.pyw + :language: python + :lines: 9- + :linenos: + +Wrapper für Test-System: + +.. literalinclude:: ../../examples/importViewUDFTest.pyw + :language: python + :lines: 9- + :linenos: + + ``copy_artikel.py`` ----------------------- Beispiel, wie Artikel inklusive Arbeitsplan und Stückliste dupliziert werden kann. diff --git a/examples/applus_configs.py b/examples/applus_configs.py index 8ff4c8d..7c477e6 100644 --- a/examples/applus_configs.py +++ b/examples/applus_configs.py @@ -7,10 +7,26 @@ # https://opensource.org/licenses/MIT. import pathlib +from PyAPplus64.applus import APplusServerConfigDescription -basedir = pathlib.Path(__file__) +basedir = basedir = pathlib.Path(__file__) # Adapt to your needs configdir = basedir.joinpath("config") serverConfYamlDeploy = configdir.joinpath("applus-server-deploy.yaml") serverConfYamlTest = configdir.joinpath("applus-server-test.yaml") serverConfYamlProd = configdir.joinpath("applus-server-prod.yaml") + + +serverConfDescProdEnv1 = APplusServerConfigDescription("Prod/Env1", serverConfYamlProd, env="Env1") +serverConfDescProdEnv2 = APplusServerConfigDescription("Prod/Env2", serverConfYamlProd, env="Env2") +serverConfDescTestEnv1 = APplusServerConfigDescription("Test/Env1", serverConfYamlTest, env="Env1") +serverConfDescTestEnv2 = APplusServerConfigDescription("Test/Env2", serverConfYamlTest, env="Env2") +serverConfDescDeploy = APplusServerConfigDescription("Deploy", serverConfYamlDeploy) + +serverConfDescs = [ + serverConfDescProdEnv1, + serverConfDescProdEnv2, + serverConfDescTestEnv1, + serverConfDescTestEnv2, + serverConfDescDeploy +] \ No newline at end of file diff --git a/examples/complete_sql.pyw b/examples/complete_sql.pyw new file mode 100644 index 0000000..a36fa47 --- /dev/null +++ b/examples/complete_sql.pyw @@ -0,0 +1,105 @@ +# Copyright (c) 2023 Thomas Tuerk (kontakt@thomas-tuerk.de) +# +# This file is part of PyAPplus64 (see https://www.thomas-tuerk.de/de/pyapplus64). +# +# Use of this source code is governed by an MIT-style +# license that can be found in the LICENSE file or at +# https://opensource.org/licenses/MIT. + +import PySimpleGUI as sg # type: ignore +import PyAPplus64 +import applus_configs +import pathlib +from typing import Tuple, Optional, Union + +try: + import sqlparse +except: + pass + +try: + import sqlfmt.api +except: + pass + +def prettyPrintSQL(format, sql): + try: + if format == "sqlfmt": + mode = sqlfmt.api.Mode(dialect_name="ClickHouse") + sqlPretty = sqlfmt.api.format_string(sql, mode) + return sqlPretty.replace("N '", "N'") # fix String Constants + elif format == "sqlparse-2": + return sqlparse.format(sql, reindent=True, keyword_case='upper') + elif format == "sqlparse": + return sqlparse.format(sql, reindent_aligned=True, keyword_case='upper') + else: + return sql + except e: + print (str(e)) + return sql + +def main() -> None: + monospaceFont = ("Courier New", 12) + sysenvs = applus_configs.serverConfDescs[:]; + sysenvs.append("-"); + layout = [ + [sg.Button("Vervollständigen"), sg.Button("aus Clipboard", key="import"), sg.Button("nach Clipboard", key="export"), sg.Button("zurücksetzen", key="clear"), sg.Button("Beenden"), + sg.Text('System/Umgebung:'), sg.Combo(sysenvs, default_value="-", key="sysenv", readonly=True), sg.Text('Formatierung:'), sg.Combo(["-", "sqlfmt", "sqlparse", "sqlparse-2"], default_value="sqlparse", key="formatieren", readonly=True) + ], + [sg.Text('Eingabe-SQL')], + [sg.Multiline(key='input', size=(150, 20), font=monospaceFont)], + [sg.Text('Ausgabe-SQL')], + [sg.Multiline(key='output', size=(150, 20), font=monospaceFont, horizontal_scroll=True)] + ] + + # server = PyAPplus64.applusFromConfigFile(confFile, user=user, env=env) + # systemName = server.scripttool.getSystemName() + "/" + server.scripttool.getMandant() + window = sg.Window("Complete SQL", layout) + oldSys = None + while True: + event, values = window.read() + if event == sg.WIN_CLOSED or event == 'Beenden': + break + elif event == 'clear': + window['input'].update("") + window['output'].update("") + elif event == 'import': + try: + window['input'].update(window.TKroot.clipboard_get()) + except: + window['input'].update("") + window['output'].update("") + elif event == 'export': + try: + window.TKroot.clipboard_clear() + window.TKroot.clipboard_append(window['output'].get()) + except: + pass + elif event == 'Vervollständigen': + sqlIn = window['input'].get() + try: + if sqlIn: + sys = window['sysenv'].get() + if sys != oldSys: + oldSys = sys + if sys and sys != "-": + server = sys.connect() + else: + server = None + if server: + sqlOut = server.completeSQL(sqlIn) + else: + sqlOut = sqlIn + sqlOut = prettyPrintSQL(window['formatieren'].get(), sqlOut) + else: + sqlOut = "" + except Exception as e: + sqlOut = "ERROR: " + str(e) + sg.popup_error_with_traceback("Fehler bei Vervollständigung", e) + window['output'].update(value=sqlOut) + + window.close() + + +if __name__ == "__main__": + main() diff --git a/examples/importViewUDF.py b/examples/importViewUDF.py new file mode 100644 index 0000000..7e7ebfa --- /dev/null +++ b/examples/importViewUDF.py @@ -0,0 +1,75 @@ +# Copyright (c) 2023 Thomas Tuerk (kontakt@thomas-tuerk.de) +# +# This file is part of PyAPplus64 (see https://www.thomas-tuerk.de/de/pyapplus64). +# +# Use of this source code is governed by an MIT-style +# license that can be found in the LICENSE file or at +# https://opensource.org/licenses/MIT. + +import PySimpleGUI as sg # type: ignore +import pathlib +import PyAPplus64 +from PyAPplus64 import applus +from PyAPplus64 import sql_utils +import applus_configs +import traceback +import pathlib +import sys + +def importViewsUDFs(server, views, udfs, dbanpass): + res = "" + try: + if views or udfs: + for env in server.scripttool.getAllEnvironments(): + res = res + server.importUdfsAndViews(env, views, udfs); + res = res + "\n\n"; + + for xml in dbanpass: + res = res + "Verarbeite " + xml + "\n" + xmlRes = server.updateDatabase(xml); + if (xmlRes == ""): xmlRes = "OK"; + res = res + xmlRes + res = res + "\n\n" + + sg.popup_scrolled("Importiere", res) + except: + sg.popup_error("Fehler", traceback.format_exc()) + +def importIntoSystem(server, system): + try: + if (len(sys.argv) < 2): + sg.popup_error("Keine Datei zum Import übergeben") + return + + views = [] + udfs = [] + dbanpass = [] + errors = "" + + + for i in range (1, len(sys.argv)): + arg = pathlib.Path(sys.argv[i]) + if arg == server.scripttool.getInstallPathAppServer().joinpath("Database", "View", arg.stem + ".sql"): + views.append(arg.stem) + elif arg == server.scripttool.getInstallPathAppServer().joinpath("Database", "UDF", arg.stem + ".sql"): + udfs.append(arg.stem) + elif arg == server.scripttool.getInstallPathAppServer().joinpath("DBChange", arg.stem + ".xml"): + dbanpass.append(arg.stem + ".xml") + else: + errors = errors + " - " + str(arg) + "\n"; + + if len(errors) > 0: + msg = "Folgende Dateien sind keine View, UDF oder DB-Anpass-Dateien des "+system+"-Systems:\n" + errors; + sg.popup_error("Fehler", msg); + if views or udfs or dbanpass: + importViewsUDFs(server, views, udfs, dbanpass) + + except: + sg.popup_error("Fehler", traceback.format_exc()) + +if __name__ == "__main__": + server = PyAPplus64.applusFromConfigFile(applus_configs.serverConfYamlDeploy) + importIntoSystem(server, "Deploy"); + + + diff --git a/examples/importViewUDFDeploy.pyw b/examples/importViewUDFDeploy.pyw new file mode 100644 index 0000000..c1116ed --- /dev/null +++ b/examples/importViewUDFDeploy.pyw @@ -0,0 +1,15 @@ +# Copyright (c) 2023 Thomas Tuerk (kontakt@thomas-tuerk.de) +# +# This file is part of PyAPplus64 (see https://www.thomas-tuerk.de/de/pyapplus64). +# +# Use of this source code is governed by an MIT-style +# license that can be found in the LICENSE file or at +# https://opensource.org/licenses/MIT. + +import importViewUDF +import applus_configs +import PyAPplus64 + +if __name__ == "__main__": + server = PyAPplus64.applusFromConfigFile(applus_configs.serverConfYamlDeploy) + importViewUDF.importIntoSystem(server, "Deploy") diff --git a/examples/importViewUDFTest.pyw b/examples/importViewUDFTest.pyw new file mode 100644 index 0000000..8fba67a --- /dev/null +++ b/examples/importViewUDFTest.pyw @@ -0,0 +1,15 @@ +# Copyright (c) 2023 Thomas Tuerk (kontakt@thomas-tuerk.de) +# +# This file is part of PyAPplus64 (see https://www.thomas-tuerk.de/de/pyapplus64). +# +# Use of this source code is governed by an MIT-style +# license that can be found in the LICENSE file or at +# https://opensource.org/licenses/MIT. + +import importViewUDF +import applus_configs +import PyAPplus64 + +if __name__ == "__main__": + server = PyAPplus64.applusFromConfigFile(applus_configs.serverConfYamlTest) + importViewUDF.importIntoSystem(server, "Test") diff --git a/examples/read_settings.py b/examples/read_settings.py index bbce997..cec87b7 100644 --- a/examples/read_settings.py +++ b/examples/read_settings.py @@ -48,8 +48,8 @@ def main(confFile: Union[str, pathlib.Path], user: Optional[str] = None, env: Op print(" InstallPathWebServer:", server.scripttool.getInstallPathWebServer()) print(" ServerInfo - Version:", server.scripttool.getServerInfo().find("version").text) - client = server.getWebClient("masterdata/artikel.asmx") - print("ARTIKEL-ASMX Date:", client.service.getServerDate()) + client = server.getWebClient("dbenv/dbenv.asmx") + print("WEB Environment:", client.service.getEnvironment()) if __name__ == "__main__": main(applus_configs.serverConfYamlTest) diff --git a/pyproject.toml b/pyproject.toml index b1ded0c..fedda18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "PyAPplus64" -version = "1.1.1" +version = "1.1.2" authors = [ { name="Thomas Tuerk", email="kontakt@thomas-tuerk.de" }, ] @@ -24,7 +24,10 @@ dependencies = [ 'SQLAlchemy', 'pandas', 'XlsxWriter', - 'zeep' + 'zeep', + 'pysimplegui', + 'sqlparse', + 'sqlfmt' ] [project.urls] diff --git a/src/PyAPplus64/applus.py b/src/PyAPplus64/applus.py index 8a33a24..d47f47e 100644 --- a/src/PyAPplus64/applus.py +++ b/src/PyAPplus64/applus.py @@ -14,6 +14,7 @@ from . import applus_scripttool from . import applus_usexml from . import sql_utils import yaml +import json import urllib.parse from zeep import Client import pyodbc # type: ignore @@ -44,12 +45,8 @@ class APplusServer: self.server_settings : applus_server.APplusServerSettings = server_settings """Einstellung für die Verbindung zum APP- und Webserver""" - self.db_conn = db_settings.connect() - """ - Eine pyodbc-Connection zur APplus DB. Diese muss genutzt werden, wenn mehrere Operationen in einer Transaktion - genutzt werden sollen. Ansonsten sind die Hilfsmethoden wie :meth:`APplusServer.dbQuery` zu bevorzugen. - Diese Connection kann in Verbindung mit den Funktionen aus :mod:`PyAPplus64.applus_db` genutzt werden. - """ + self._db_conn_pool = list() + """Eine Liste bestehender DB-Verbindungen""" self.server_conn: applus_server.APplusServerConnection = applus_server.APplusServerConnection(server_settings) """erlaubt den Zugriff auf den AppServer""" @@ -63,18 +60,61 @@ class APplusServer: self.scripttool: applus_scripttool.APplusScriptTool = applus_scripttool.APplusScriptTool(self) """erlaubt den einfachen Zugriff auf Funktionen des ScriptTools""" - self.client_table = self.server_conn.getAppClient("p2core", "Table") - self.client_xml = self.server_conn.getAppClient("p2core", "XML") - self.client_nummer = self.server_conn.getAppClient("p2system", "Nummer") - self.client_adaptdb = self.server_conn.getAppClient("p2dbtools", "AdaptDB"); + self._client_table = None + self._client_xml = None + self._client_nummer = None + self._client_adaptdb= None + @property + def client_table(self) -> Client: + if not self._client_table: + self._client_table = self.getAppClient("p2core", "Table") + return self._client_table + + @property + def client_xml(self) -> Client: + if not self._client_xml: + self._client_xml = self.getAppClient("p2core", "XML") + return self._client_xml + + @property + def client_nummer(self) -> Client: + if not self._client_nummer: + self._client_nummer = self.getAppClient("p2system", "Nummer") + return self._client_nummer + + @property + def client_adaptdb(self) -> Client: + if not self._client_adaptdb: + self._client_adaptdb = self.getAppClient("p2dbtools", "AdaptDB") + return self._client_adaptdb + + + def getDBConnection(self) -> pyodbc.Connection: + """ + Liefert eine pyodbc-Connection zur APplus DB. Diese muss genutzt werden, wenn mehrere Operationen in einer Transaktion + genutzt werden sollen. Ansonsten sind die Hilfsmethoden wie :meth:`APplusServer.dbQuery` zu bevorzugen. + Diese Connection kann in Verbindung mit den Funktionen aus :mod:`PyAPplus64.applus_db` genutzt werden. + Die Verbindung sollte nach Benutzung wieder freigegeben oder geschlossen werden. + """ + if self._db_conn_pool: + return self._db_conn_pool.pop() + else: + conn = self.db_settings.connect() + self._db_conn_pool.append(conn) + return conn + + def releaseDBConnection(self, conn : pyodbc.Connection) -> None: + """Gibt eine DB-Connection zur Wiederverwendung frei""" + self._db_conn_pool.append(conn) def reconnectDB(self) -> None: - try: - self.db_conn.close() - except: - pass - self.db_conn = self.db_settings.connect() + for conn in self._db_conn_pool: + try: + conn.close() + except: + pass + self._db_conn_pool = list() def completeSQL(self, sql: sql_utils.SqlStatement, raw: bool = False) -> str: """ @@ -97,7 +137,11 @@ class APplusServer: """Führt eine SQL Query aus und liefert alle Zeilen zurück. Das SQL wird zunächst vom Server angepasst, so dass z.B. Mandanteninformation hinzugefügt werden.""" sqlC = self.completeSQL(sql, raw=raw) - return applus_db.rawQueryAll(self.db_conn, sqlC, *args, apply=apply) + conn = self.getDBConnection() + res = applus_db.rawQueryAll(conn, sqlC, *args, apply=apply) + self.releaseDBConnection(conn) + return res + def dbQuerySingleValues(self, sql: sql_utils.SqlStatement, *args: Any, raw: bool = False) -> Sequence[Any]: """Führt eine SQL Query aus, die nur eine Spalte zurückliefern soll.""" @@ -107,12 +151,19 @@ class APplusServer: """Führt eine SQL Query aus und führt für jede Zeile die übergeben Funktion aus. Das SQL wird zunächst vom Server angepasst, so dass z.B. Mandanteninformation hinzugefügt werden.""" sqlC = self.completeSQL(sql, raw=raw) - applus_db.rawQuery(self.db_conn, sqlC, f, *args) + conn = self.getDBConnection() + res = applus_db.rawQuery(conn, sqlC, f, *args) + self.releaseDBConnection(conn) + return res + def dbQuerySingleRow(self, sql: sql_utils.SqlStatement, *args: Any, raw: bool = False) -> Optional[pyodbc.Row]: """Führt eine SQL Query aus, die maximal eine Zeile zurückliefern soll. Diese Zeile wird geliefert.""" sqlC = self.completeSQL(sql, raw=raw) - return applus_db.rawQuerySingleRow(self.db_conn, sqlC, *args) + conn = self.getDBConnection() + res = applus_db.rawQuerySingleRow(conn, sqlC, *args) + self.releaseDBConnection(conn) + return res def dbQuerySingleRowDict(self, sql: sql_utils.SqlStatement, *args: Any, raw: bool = False) -> Optional[Dict[str, Any]]: """Führt eine SQL Query aus, die maximal eine Zeile zurückliefern soll. @@ -127,13 +178,19 @@ class APplusServer: """Führt eine SQL Query aus, die maximal einen Wert zurückliefern soll. Dieser Wert oder None wird geliefert.""" sqlC = self.completeSQL(sql, raw=raw) - return applus_db.rawQuerySingleValue(self.db_conn, sqlC, *args) + conn = self.getDBConnection() + res = applus_db.rawQuerySingleValue(conn, sqlC, *args) + self.releaseDBConnection(conn) + return res def dbExecute(self, sql: sql_utils.SqlStatement, *args: Any, raw: bool = False) -> Any: """Führt ein SQL Statement (z.B. update oder insert) aus. Das SQL wird zunächst vom Server angepasst, so dass z.B. Mandanteninformation hinzugefügt werden.""" sqlC = self.completeSQL(sql, raw=raw) - return applus_db.rawExecute(self.db_conn, sqlC, *args) + conn = self.getDBConnection() + res = applus_db.rawExecute(conn, sqlC, *args) + self.releaseDBConnection(conn) + return res def isDBTableKnown(self, table: str) -> bool: """Prüft, ob eine Tabelle im System bekannt ist""" @@ -164,6 +221,8 @@ class APplusServer: Als parameter wird die relative URL der ASMX-Seite erwartet. Die Base-URL automatisch ergänzt. Ein Beispiel für eine solche relative URL ist "masterdata/artikel.asmx". + ACHTUNG: Als Umgebung wird die Umgebung des sich anmeldenden Nutzers verwendet. Sowohl Nutzer als auch Umgebung können sich von den für App-Clients verwendeten Werten unterscheiden. Wenn möglich, sollte ein App-Client verwendet werden. + :param url: die relative URL der ASMX Seite, z.B. "masterdata/artikel.asmx" :type package: str :return: den Client @@ -197,7 +256,10 @@ class APplusServer: Jeder Eintrag enthält eine Liste von Feldern, die zusammen eindeutig sein müssen. """ - return applus_db.getUniqueFieldsOfTable(self.db_conn, table) + conn = self.getDBConnection() + res = applus_db.getUniqueFieldsOfTable(conn, table) + self.releaseDBConnection(conn) + return res def useXML(self, xml: str) -> Any: """Ruft ``p2core.xml.usexml`` auf. Wird meist durch ein ``UseXMLRow-Objekt`` aufgerufen.""" @@ -261,6 +323,33 @@ class APplusServer: return res + def importUdfsAndViews(self, environment : str, views : [str] = [], udfs : [str] = []) -> str: + """ + Importiert bestimmte Views und UDFs + :param environment: die Umgebung, in die Importiert werden soll + :type environment: string + :param views: Views, die importiert werden sollen + :type views: [string] + :param udfs: Views, die importiert werden sollen + :type udfs: [string] + :return: Infos zur Ausführung + :rtype: str + """ + lbl=""; + files=[]; + for v in views: + files.append({"type" : 1, "name" : v}) + for u in udfs: + files.append({"type" : 0, "name" : u}) + + + jobId = self.job.createSOAPJob("importing UDFs and Views"); + self.client_adaptdb.service.importUdfsAndViews(jobId, environment, False, json.dumps(files), "de"); + res = self.job.getResultURLString(jobId) + if res is None: res = "FEHLER"; + return res + + def makeWebLink(self, base: str, **kwargs: Any) -> str: if not self.server_settings.webserver: raise Exception("keine Webserver-BaseURL gesetzt") @@ -286,6 +375,14 @@ class APplusServer: def makeWebLinkBauftrag(self, **kwargs: Any) -> str: return self.makeWebLink("wp/bauftragRec.aspx", **kwargs) + def makeWebLinkAuftrag(self, **kwargs: Any) -> str: + return self.makeWebLink("sales/auftragRec.aspx", **kwargs) + + def makeWebLinkVKRahmen(self, **kwargs: Any) -> str: + return self.makeWebLink("sales/vkrahmenRec.aspx", **kwargs) + + def makeWebLinkWarenaugang(self, **kwargs: Any) -> str: + return self.makeWebLink("sales/warenausgangRec.aspx", **kwargs) def applusFromConfigDict(yamlDict: Dict[str, Any], user: Optional[str] = None, env: Optional[str] = None) -> APplusServer: """Läd Einstellungen aus einer Config und erzeugt daraus ein APplus-Objekt""" @@ -324,3 +421,39 @@ def applusFromConfig(yamlString: str, user: Optional[str] = None, env: Optional[ """Läd Einstellungen aus einer Config-Datei und erzeugt daraus ein APplus-Objekt""" yamlDict = yaml.safe_load(yamlString) return applusFromConfigDict(yamlDict, user=user, env=env) + + +class APplusServerConfigDescription: + """ + Beschreibung einer Configuration bestehend aus Config-Datei, Nutzer und Umgebung. + + :param descr: Beschreibung als String, nur für Ausgabe gedacht + :type descr: str + + :param yamlfile: die Datei + :type yamlfile: 'FileDescriptorOrPath' + + :param user: der Nutzer + :type user: Optional[str] + + :param env: die Umgebung + :type env: Optional[str] + """ + def __init__(self, + descr: str, + yamlfile: 'FileDescriptorOrPath', + user:Optional[str] = None, + env:Optional[str] = None): + self.descr = descr + self.yamlfile = yamlfile + self.user = user + self.env = env + + def __str__(self) -> str: + return self.descr + + def connect(self) -> APplusServer: + return applusFromConfigFile(self.yamlfile, user=self.user, env=self.env) + + + diff --git a/src/PyAPplus64/applus_job.py b/src/PyAPplus64/applus_job.py index 2911f3a..fd34175 100644 --- a/src/PyAPplus64/applus_job.py +++ b/src/PyAPplus64/applus_job.py @@ -7,6 +7,7 @@ # https://opensource.org/licenses/MIT. from typing import TYPE_CHECKING, Optional +from zeep import Client import uuid if TYPE_CHECKING: @@ -23,7 +24,14 @@ class APplusJob: """ def __init__(self, server: 'APplusServer') -> None: - self.client = server.getAppClient("p2core", "Job") + self.server = server + self._client = None + + @property + def client(self) -> Client: + if not self._client: + self._client = self.server.getAppClient("p2core", "Job") + return self._client def createSOAPJob(self, bez: str) -> str: """ diff --git a/src/PyAPplus64/applus_scripttool.py b/src/PyAPplus64/applus_scripttool.py index 3b3b2c6..b253c5b 100644 --- a/src/PyAPplus64/applus_scripttool.py +++ b/src/PyAPplus64/applus_scripttool.py @@ -10,6 +10,7 @@ from .applus import APplusServer from . import sql_utils import lxml.etree as ET # type: ignore from typing import Optional, Tuple, Set +from zeep import Client import pathlib @@ -57,7 +58,14 @@ class APplusScriptTool: """ def __init__(self, server: APplusServer) -> None: - self.client = server.getAppClient("p2script", "ScriptTool") + self.server = server + self._client = None + + @property + def client(self) -> Client: + if not self._client: + self._client = self.server.getAppClient("p2script", "ScriptTool") + return self._client def getCurrentDate(self) -> str: return self.client.service.getCurrentDate() @@ -181,3 +189,18 @@ class APplusScriptTool: :rtype: ET.Element """ return ET.fromstring(self.getServerInfoString()) + + def getAllEnvironments(self) -> [str]: + """ + Liefert alle Umgebungen + + :return: die gefundenen Umgebungen + :rtype: [str] + """ + + envs = [] + envString = self.client.service.getAllEnvironmentsInMasterDatabase() + for e in envString.split(","): + envs.append(e.split(":")[0]) + return envs + diff --git a/src/PyAPplus64/applus_server.py b/src/PyAPplus64/applus_server.py index db0ce01..fa35973 100644 --- a/src/PyAPplus64/applus_server.py +++ b/src/PyAPplus64/applus_server.py @@ -28,7 +28,7 @@ class APplusServerSettings: self.appserver = appserver self.appserverPort = appserverPort self.user = user - self.env = env + self.env = env self.webserver = webserver self.webserverUser = webserverUser @@ -101,6 +101,8 @@ class APplusServerConnection: Als parameter wird die relative URL der ASMX-Seite erwartet. Die Base-URL automatisch ergänzt. Ein Beispiel für eine solche relative URL ist "masterdata/artikel.asmx". + ACHTUNG: Als Umgebung wird die Umgebung des sich anmeldenden Nutzers verwendet. Sowohl Nutzer als auch Umgebung können sich von den für App-Clients verwendeten Werten unterscheiden. Wenn möglich, sollte ein App-Client verwendet werden. + :param url: die relative URL der ASMX Seite, z.B. "masterdata/artikel.asmx" :type package: str :return: den Client diff --git a/src/PyAPplus64/applus_sysconf.py b/src/PyAPplus64/applus_sysconf.py index adb154c..a047c83 100644 --- a/src/PyAPplus64/applus_sysconf.py +++ b/src/PyAPplus64/applus_sysconf.py @@ -7,6 +7,7 @@ # https://opensource.org/licenses/MIT. from typing import TYPE_CHECKING, Optional, Dict, Any, Callable, Sequence +from zeep import Client if TYPE_CHECKING: from .applus import APplusServer @@ -22,8 +23,16 @@ class APplusSysConf: """ def __init__(self, server: 'APplusServer') -> None: - self.client = server.getAppClient("p2system", "SysConf") self.cache: Dict[str, type] = {} + self.server = server + self._client = None + + @property + def client(self) -> Client: + if not self._client: + self._client = self.server.getAppClient("p2system", "SysConf") + return self._client + def clearCache(self) -> None: self.cache = {} diff --git a/src/PyAPplus64/sql_utils.py b/src/PyAPplus64/sql_utils.py index 8a50e73..3d8e2b4 100644 --- a/src/PyAPplus64/sql_utils.py +++ b/src/PyAPplus64/sql_utils.py @@ -21,6 +21,7 @@ APplus. Oft ist es sinnvoll, solche Parameter zu verwenden. from __future__ import annotations import datetime +import numpy from typing import Set, Sequence, Union, Optional, cast, List @@ -158,7 +159,7 @@ def formatSqlValue(v: SqlValue) -> str: if v is None: raise Exception("formatSqlValue: null not supported") - if isinstance(v, (int, float, SqlField)): + if isinstance(v, (int, float, SqlField, numpy.int64)): return str(v) elif isinstance(v, str): return formatSqlValueString(v)