Verbindungsaufbau nur wenn nötig

This commit is contained in:
Thomas Türk 2023-11-13 12:36:21 +01:00
parent e7fe3fb037
commit f5a4342bcf
17 changed files with 490 additions and 39 deletions

View File

@ -46,3 +46,9 @@ Pandas / SqlAlchemy / xlsxwriter
Sollen Excel-Dateien mit Pandas erzeugt, werden, so muss Pandas, SqlAlchemy und xlsxwriter installiert sein Sollen Excel-Dateien mit Pandas erzeugt, werden, so muss Pandas, SqlAlchemy und xlsxwriter installiert sein
(`python -m pip install pandas sqlalchemy xlsxwriter`). (`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.

View File

@ -4,10 +4,11 @@ typische Anwendungsfälle
einfache Admin-Aufgaben einfache Admin-Aufgaben
----------------------- -----------------------
Selten auftretende Admin-Aufgaben lassen sich gut mittels Python-Scripten automatisieren. Selten auftretende Admin-Aufgaben lassen sich gut mittels Python-Scripten
Es ist sehr einfach möglich, auf die DB, aber auch auf SOAP-Schnittstelle zuzugreifen. automatisieren. Es ist sehr einfach möglich, auf die DB, aber auch auf
Ich habe dies vor allem für Wartungsarbeiten an Anpassungstabellen, die für eigene Erweiterungen SOAP-Schnittstelle der APP-Serverse zuzugreifen. Zudem ist rudimentärer Zugriff
entwickelt wurden, genutzt. 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 Als triviales Beispiel sucht folgender Code alle `DOCUMENTS` Einträge in
Artikeln (angezeigt als `Bild` in `ArtikelRec.aspx`), für die Datei, auf die 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 Beispielsweise kann auf die Sys-Config auch händisch, d.h. durch direkten Aufruf einer SOAP-Methode
des APP-Servers zugegriffen werden:: des APP-Servers zugegriffen werden::
client = server.server_conn.getAppClient("p2system", "SysConf"); client = server.getAppClient("p2system", "SysConf");
print (client.service.getString("STAMM", "MYLAND")) print (client.service.getString("STAMM", "MYLAND"))

View File

@ -14,8 +14,8 @@ sys.path.append('../src/')
project = 'PyAPplus64' project = 'PyAPplus64'
copyright = '2023, Thomas Tuerk' copyright = '2023, Thomas Tuerk'
author = 'Thomas Tuerk' author = 'Thomas Tuerk'
version = '1.1.1' version = '1.1.2'
release = '1.1.1' release = '1.1.2'
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

View File

@ -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 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, 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 .. literalinclude:: ../../examples/applus_configs.py
:language: python :language: python
@ -74,6 +75,44 @@ Die GUI wird um die Erzeugung von Excel-Dateien mit Mengenabweichungen gebaut.
:lines: 9- :lines: 9-
:linenos: :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`` ``copy_artikel.py``
----------------------- -----------------------
Beispiel, wie Artikel inklusive Arbeitsplan und Stückliste dupliziert werden kann. Beispiel, wie Artikel inklusive Arbeitsplan und Stückliste dupliziert werden kann.

View File

@ -7,10 +7,26 @@
# https://opensource.org/licenses/MIT. # https://opensource.org/licenses/MIT.
import pathlib import pathlib
from PyAPplus64.applus import APplusServerConfigDescription
basedir = pathlib.Path(__file__) basedir = basedir = pathlib.Path(__file__) # Adapt to your needs
configdir = basedir.joinpath("config") configdir = basedir.joinpath("config")
serverConfYamlDeploy = configdir.joinpath("applus-server-deploy.yaml") serverConfYamlDeploy = configdir.joinpath("applus-server-deploy.yaml")
serverConfYamlTest = configdir.joinpath("applus-server-test.yaml") serverConfYamlTest = configdir.joinpath("applus-server-test.yaml")
serverConfYamlProd = configdir.joinpath("applus-server-prod.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
]

105
examples/complete_sql.pyw Normal file
View File

@ -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()

75
examples/importViewUDF.py Normal file
View File

@ -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");

View File

@ -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")

View File

@ -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")

View File

@ -48,8 +48,8 @@ def main(confFile: Union[str, pathlib.Path], user: Optional[str] = None, env: Op
print(" InstallPathWebServer:", server.scripttool.getInstallPathWebServer()) print(" InstallPathWebServer:", server.scripttool.getInstallPathWebServer())
print(" ServerInfo - Version:", server.scripttool.getServerInfo().find("version").text) print(" ServerInfo - Version:", server.scripttool.getServerInfo().find("version").text)
client = server.getWebClient("masterdata/artikel.asmx") client = server.getWebClient("dbenv/dbenv.asmx")
print("ARTIKEL-ASMX Date:", client.service.getServerDate()) print("WEB Environment:", client.service.getEnvironment())
if __name__ == "__main__": if __name__ == "__main__":
main(applus_configs.serverConfYamlTest) main(applus_configs.serverConfYamlTest)

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "PyAPplus64" name = "PyAPplus64"
version = "1.1.1" version = "1.1.2"
authors = [ authors = [
{ name="Thomas Tuerk", email="kontakt@thomas-tuerk.de" }, { name="Thomas Tuerk", email="kontakt@thomas-tuerk.de" },
] ]
@ -24,7 +24,10 @@ dependencies = [
'SQLAlchemy', 'SQLAlchemy',
'pandas', 'pandas',
'XlsxWriter', 'XlsxWriter',
'zeep' 'zeep',
'pysimplegui',
'sqlparse',
'sqlfmt'
] ]
[project.urls] [project.urls]

View File

@ -14,6 +14,7 @@ from . import applus_scripttool
from . import applus_usexml from . import applus_usexml
from . import sql_utils from . import sql_utils
import yaml import yaml
import json
import urllib.parse import urllib.parse
from zeep import Client from zeep import Client
import pyodbc # type: ignore import pyodbc # type: ignore
@ -44,12 +45,8 @@ class APplusServer:
self.server_settings : applus_server.APplusServerSettings = server_settings self.server_settings : applus_server.APplusServerSettings = server_settings
"""Einstellung für die Verbindung zum APP- und Webserver""" """Einstellung für die Verbindung zum APP- und Webserver"""
self.db_conn = db_settings.connect() self._db_conn_pool = list()
""" """Eine Liste bestehender DB-Verbindungen"""
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.server_conn: applus_server.APplusServerConnection = applus_server.APplusServerConnection(server_settings) self.server_conn: applus_server.APplusServerConnection = applus_server.APplusServerConnection(server_settings)
"""erlaubt den Zugriff auf den AppServer""" """erlaubt den Zugriff auf den AppServer"""
@ -63,18 +60,61 @@ class APplusServer:
self.scripttool: applus_scripttool.APplusScriptTool = applus_scripttool.APplusScriptTool(self) self.scripttool: applus_scripttool.APplusScriptTool = applus_scripttool.APplusScriptTool(self)
"""erlaubt den einfachen Zugriff auf Funktionen des ScriptTools""" """erlaubt den einfachen Zugriff auf Funktionen des ScriptTools"""
self.client_table = self.server_conn.getAppClient("p2core", "Table") self._client_table = None
self.client_xml = self.server_conn.getAppClient("p2core", "XML") self._client_xml = None
self.client_nummer = self.server_conn.getAppClient("p2system", "Nummer") self._client_nummer = None
self.client_adaptdb = self.server_conn.getAppClient("p2dbtools", "AdaptDB"); 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: def reconnectDB(self) -> None:
for conn in self._db_conn_pool:
try: try:
self.db_conn.close() conn.close()
except: except:
pass pass
self.db_conn = self.db_settings.connect() self._db_conn_pool = list()
def completeSQL(self, sql: sql_utils.SqlStatement, raw: bool = False) -> str: 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 """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.""" vom Server angepasst, so dass z.B. Mandanteninformation hinzugefügt werden."""
sqlC = self.completeSQL(sql, raw=raw) 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]: 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.""" """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. """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.""" Das SQL wird zunächst vom Server angepasst, so dass z.B. Mandanteninformation hinzugefügt werden."""
sqlC = self.completeSQL(sql, raw=raw) 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]: 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.""" """Führt eine SQL Query aus, die maximal eine Zeile zurückliefern soll. Diese Zeile wird geliefert."""
sqlC = self.completeSQL(sql, raw=raw) 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]]: 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. """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. """Führt eine SQL Query aus, die maximal einen Wert zurückliefern soll.
Dieser Wert oder None wird geliefert.""" Dieser Wert oder None wird geliefert."""
sqlC = self.completeSQL(sql, raw=raw) 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: 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 """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.""" vom Server angepasst, so dass z.B. Mandanteninformation hinzugefügt werden."""
sqlC = self.completeSQL(sql, raw=raw) 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: def isDBTableKnown(self, table: str) -> bool:
"""Prüft, ob eine Tabelle im System bekannt ist""" """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. 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". 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" :param url: die relative URL der ASMX Seite, z.B. "masterdata/artikel.asmx"
:type package: str :type package: str
:return: den Client :return: den Client
@ -197,7 +256,10 @@ class APplusServer:
Jeder Eintrag enthält eine Liste von Feldern, die zusammen eindeutig sein Jeder Eintrag enthält eine Liste von Feldern, die zusammen eindeutig sein
müssen. 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: def useXML(self, xml: str) -> Any:
"""Ruft ``p2core.xml.usexml`` auf. Wird meist durch ein ``UseXMLRow-Objekt`` aufgerufen.""" """Ruft ``p2core.xml.usexml`` auf. Wird meist durch ein ``UseXMLRow-Objekt`` aufgerufen."""
@ -261,6 +323,33 @@ class APplusServer:
return res 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: def makeWebLink(self, base: str, **kwargs: Any) -> str:
if not self.server_settings.webserver: if not self.server_settings.webserver:
raise Exception("keine Webserver-BaseURL gesetzt") raise Exception("keine Webserver-BaseURL gesetzt")
@ -286,6 +375,14 @@ class APplusServer:
def makeWebLinkBauftrag(self, **kwargs: Any) -> str: def makeWebLinkBauftrag(self, **kwargs: Any) -> str:
return self.makeWebLink("wp/bauftragRec.aspx", **kwargs) 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: 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""" """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""" """Läd Einstellungen aus einer Config-Datei und erzeugt daraus ein APplus-Objekt"""
yamlDict = yaml.safe_load(yamlString) yamlDict = yaml.safe_load(yamlString)
return applusFromConfigDict(yamlDict, user=user, env=env) 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)

View File

@ -7,6 +7,7 @@
# https://opensource.org/licenses/MIT. # https://opensource.org/licenses/MIT.
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
from zeep import Client
import uuid import uuid
if TYPE_CHECKING: if TYPE_CHECKING:
@ -23,7 +24,14 @@ class APplusJob:
""" """
def __init__(self, server: 'APplusServer') -> None: 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: def createSOAPJob(self, bez: str) -> str:
""" """

View File

@ -10,6 +10,7 @@ from .applus import APplusServer
from . import sql_utils from . import sql_utils
import lxml.etree as ET # type: ignore import lxml.etree as ET # type: ignore
from typing import Optional, Tuple, Set from typing import Optional, Tuple, Set
from zeep import Client
import pathlib import pathlib
@ -57,7 +58,14 @@ class APplusScriptTool:
""" """
def __init__(self, server: APplusServer) -> None: 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: def getCurrentDate(self) -> str:
return self.client.service.getCurrentDate() return self.client.service.getCurrentDate()
@ -181,3 +189,18 @@ class APplusScriptTool:
:rtype: ET.Element :rtype: ET.Element
""" """
return ET.fromstring(self.getServerInfoString()) 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

View File

@ -101,6 +101,8 @@ class APplusServerConnection:
Als parameter wird die relative URL der ASMX-Seite erwartet. Die Base-URL automatisch ergänzt. 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". 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" :param url: die relative URL der ASMX Seite, z.B. "masterdata/artikel.asmx"
:type package: str :type package: str
:return: den Client :return: den Client

View File

@ -7,6 +7,7 @@
# https://opensource.org/licenses/MIT. # https://opensource.org/licenses/MIT.
from typing import TYPE_CHECKING, Optional, Dict, Any, Callable, Sequence from typing import TYPE_CHECKING, Optional, Dict, Any, Callable, Sequence
from zeep import Client
if TYPE_CHECKING: if TYPE_CHECKING:
from .applus import APplusServer from .applus import APplusServer
@ -22,8 +23,16 @@ class APplusSysConf:
""" """
def __init__(self, server: 'APplusServer') -> None: def __init__(self, server: 'APplusServer') -> None:
self.client = server.getAppClient("p2system", "SysConf")
self.cache: Dict[str, type] = {} 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: def clearCache(self) -> None:
self.cache = {} self.cache = {}

View File

@ -21,6 +21,7 @@ APplus. Oft ist es sinnvoll, solche Parameter zu verwenden.
from __future__ import annotations from __future__ import annotations
import datetime import datetime
import numpy
from typing import Set, Sequence, Union, Optional, cast, List from typing import Set, Sequence, Union, Optional, cast, List
@ -158,7 +159,7 @@ def formatSqlValue(v: SqlValue) -> str:
if v is None: if v is None:
raise Exception("formatSqlValue: null not supported") raise Exception("formatSqlValue: null not supported")
if isinstance(v, (int, float, SqlField)): if isinstance(v, (int, float, SqlField, numpy.int64)):
return str(v) return str(v)
elif isinstance(v, str): elif isinstance(v, str):
return formatSqlValueString(v) return formatSqlValueString(v)