Compare commits
3 Commits
Author | SHA1 | Date |
---|---|---|
Thomas Türk | f5a4342bcf | |
Thomas Türk | e7fe3fb037 | |
Thomas Türk | 599339a270 |
10
Changelog.md
10
Changelog.md
|
@ -1,5 +1,15 @@
|
|||
# Changelog
|
||||
|
||||
## 23.08.2023 v1.1.1
|
||||
- `APplusJob` für einfachen Zugriff auf `p2core.Job` implementiert
|
||||
- Funktion für Ausführung von DB-Anpass Dateien implementiert
|
||||
|
||||
## 27.07.2023 v1.1.0
|
||||
- implementiere Zugriff auf ASMX Seiten
|
||||
- `getClient` -> `getAppClient` umbenannt
|
||||
- `getWebClient` implementiert
|
||||
- dies benötigt Paket `requests-negotiate-sspi`, das leider nur für Windows verfügbar ist
|
||||
|
||||
## 06.05.2023 v1.0.1
|
||||
- Code-Cleanup mit Hilfe von flake8
|
||||
- Bugfix: neue Python 3.10 Syntax entfernt
|
||||
|
|
28
README.md
28
README.md
|
@ -1,21 +1,21 @@
|
|||
# PyAPplus64
|
||||
|
||||
## Beschreibung
|
||||
Das Paket `PyAPplus64` enthält eine Sammlung von Python Tools für die Interaktion mit dem ERP-System APplus 6.4.
|
||||
Es sollte auch für andere APplus Versionen nützlich sein.
|
||||
## Beschreibung
|
||||
Das Paket `PyAPplus64` enthält eine Sammlung von Python Tools für die Interaktion mit dem ERP-System APplus 6.4.
|
||||
Es sollte auch für andere APplus Versionen nützlich sein.
|
||||
|
||||
Zielgruppe sind APplus-Administratoren und Anpassungs-Entwickler. Die Tools erlauben u.a.
|
||||
|
||||
- einfacher Zugriff auf SOAP-Schnittstelle des App-Servers
|
||||
+ damit Zugriff auf SysConfig
|
||||
+ Zugriff auf Tools `nextNumber` für Erzeugung der nächsten Nummer für ein Business-Object
|
||||
+ ...
|
||||
+ ...
|
||||
- Zugriff auf APplus DB per direktem DB-Zugriff und mittels SOAP
|
||||
+ automatischer Aufruf von `completeSQL`, um per App-Server SQL-Statements um z.B. Mandanten erweitern zu lassen
|
||||
+ Tools für einfache Benutzung von `useXML`, d.h. für das Einfügen, Löschen und Ändern von Datensätzen
|
||||
mit Hilfe des APP-Servers. Genau wie bei Änderungen an Datensätzen über die Web-Oberfläche und im Gegensatz
|
||||
zum direkten Zugriff über die Datenbank werden dabei evtl. zusätzliche
|
||||
Checks ausgeführt, bestimmte Felder automatisch gesetzt oder bestimmte Aktionen angestoßen.
|
||||
mit Hilfe des APP-Servers. Genau wie bei Änderungen an Datensätzen über die Web-Oberfläche und im Gegensatz
|
||||
zum direkten Zugriff über die Datenbank werden dabei evtl. zusätzliche
|
||||
Checks ausgeführt, bestimmte Felder automatisch gesetzt oder bestimmte Aktionen angestoßen.
|
||||
- das Duplizieren von Datensätzen
|
||||
+ zu kopierende Felder aus XML-Definitionen werden ausgewertet
|
||||
+ Abhängige Objekte können einfach ebenfalls mit-kopiert werden
|
||||
|
@ -35,9 +35,9 @@ aus, dass im Laufe der Zeit weitere Features hinzukommen.
|
|||
|
||||
## Warnung
|
||||
|
||||
`PyAPplus64` erlaubt den schreibenden Zugriff auf die APplus Datenbank und beliebige
|
||||
Aufrufe von SOAP-Methoden. Unsachgemäße Nutzung kann Ihre Daten zerstören. Benutzen Sie
|
||||
`PyAPplus64` daher bitte vorsichtig.
|
||||
`PyAPplus64` erlaubt den schreibenden Zugriff auf die APplus Datenbank und beliebige
|
||||
Aufrufe von SOAP-Methoden. Unsachgemäße Nutzung kann Ihre Daten zerstören. Benutzen Sie
|
||||
`PyAPplus64` daher bitte vorsichtig.
|
||||
|
||||
## Installation
|
||||
|
||||
|
@ -47,6 +47,14 @@ PyAPplus64 wurde auf PyPi veröffentlicht. Es lässt sich daher einfach mittel `
|
|||
pip install PyAPplus64
|
||||
````
|
||||
|
||||
Zur Nutzung von ASMX-Seiten ist die Authentifizierungsmethode Negotiate nötig. Für diese muss `requests-negotiate-sspi` installiert werden,
|
||||
was aber leider nur unter Windows verfügbar ist.
|
||||
|
||||
````
|
||||
pip install requests-negotiate-sspi
|
||||
````
|
||||
|
||||
|
||||
## Links
|
||||
|
||||
- [PyPi](https://pypi.org/project/PyAPplus64/)
|
||||
|
|
|
@ -3,8 +3,8 @@ Abhängigkeiten
|
|||
|
||||
pyodbc
|
||||
------
|
||||
Für die Datenbankverbindung wird ``pyodbc`` (``python -m pip install pyodbc``) verwendet.
|
||||
Der passende ODBC Treiber, MS SQL Server 2012 Native Client, wird zusätzlich benötigt.
|
||||
Für die Datenbankverbindung wird ``pyodbc`` (``python -m pip install pyodbc``) verwendet.
|
||||
Der passende ODBC Treiber, MS SQL Server 2012 Native Client, wird zusätzlich benötigt.
|
||||
Dieser kann von Microsoft bezogen werden.
|
||||
|
||||
|
||||
|
@ -13,18 +13,25 @@ zeep
|
|||
Die Soap-Library ``zeep`` wird benutzt (``python -m pip install zeep``).
|
||||
|
||||
|
||||
requests-negotiate-sspi
|
||||
-----------------------
|
||||
Die Authentifzierungsmethode Negotiate Wird für Zugriffe auf ASMX-Seiten benutzt (``python -m pip install requests-negotiate-sspi``).
|
||||
Leider ist dies nur unter Windows verfügbar. Alle anderen Funktionen können aber auch ohne
|
||||
dieses Paket benutzt werden.
|
||||
|
||||
|
||||
PyYaml
|
||||
------
|
||||
|
||||
Die Library ``pyyaml`` wird für Config-Dateien benutzt (``python -m pip install pyyaml``).
|
||||
|
||||
|
||||
Sphinx
|
||||
Sphinx
|
||||
------
|
||||
Diese Dokumentation ist mit Sphinx geschrieben.
|
||||
``python -m pip install sphinx``. Dokumentation ist im Unterverzeichnis
|
||||
`docs` zu finden. Sie kann mittels ``make.bat html`` erzeugt werden,
|
||||
dies ruft intern ``sphinx-build -M html source build`` auf. Die Dokumentation
|
||||
Diese Dokumentation ist mit Sphinx geschrieben.
|
||||
``python -m pip install sphinx``. Dokumentation ist im Unterverzeichnis
|
||||
`docs` zu finden. Sie kann mittels ``make.bat html`` erzeugt werden,
|
||||
dies ruft intern ``sphinx-build -M html source build`` auf. Die Dokumentation
|
||||
der Python-API sollte evtl. vorher
|
||||
mittels ``sphinx-apidoc -T -f ../src/PyAPplus64 -o source/generated`` erzeugt
|
||||
oder aktualisiert werden. Evtl. können 2 Aufrufe von ``make.bat html`` sinnvoll
|
||||
|
@ -36,5 +43,12 @@ Die erzeugte Doku findet sich im Verzeichnis ``build/html``.
|
|||
|
||||
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`).
|
||||
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.
|
||||
|
|
|
@ -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
|
||||
|
@ -85,10 +86,10 @@ Zugriff auf die Sysconf möglich::
|
|||
print (server.sysconf.getList("STAMM", "EULAENDER"))
|
||||
|
||||
Dank der Bibliothek `zeep` ist es auch sehr einfach möglich, auf beliebige SOAP-Methoden zuzugreifen.
|
||||
Beispielsweise kann auf die Sys-Config auch händisch, d.h. durch direkten Aufruf einer SOAP-Methode,
|
||||
zugegriffen werden::
|
||||
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.getClient("p2system", "SysConf");
|
||||
client = server.getAppClient("p2system", "SysConf");
|
||||
print (client.service.getString("STAMM", "MYLAND"))
|
||||
|
||||
|
||||
|
|
|
@ -14,8 +14,8 @@ sys.path.append('../src/')
|
|||
project = 'PyAPplus64'
|
||||
copyright = '2023, Thomas Tuerk'
|
||||
author = 'Thomas Tuerk'
|
||||
version = '1.0.1'
|
||||
release = '1.0.1'
|
||||
version = '1.1.2'
|
||||
release = '1.1.2'
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -20,6 +20,14 @@ PyAPplus64.applus\_db module
|
|||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
PyAPplus64.applus\_job module
|
||||
-----------------------------
|
||||
|
||||
.. automodule:: PyAPplus64.applus_job
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
PyAPplus64.applus\_scripttool module
|
||||
------------------------------------
|
||||
|
||||
|
|
|
@ -16,7 +16,10 @@ appserver : {
|
|||
env : "default-umgebung" # hier wirklich Umgebung, nicht Mandant verwenden
|
||||
}
|
||||
webserver : {
|
||||
baseurl : "http://some-server/APplusProd6/"
|
||||
baseurl : "http://some-server/APplusProd6/",
|
||||
user : null, # oft "ASOL.Projects", wenn nicht gesetzt, wird aktueller Windows-Nutzer verwendet
|
||||
userDomain : null, # Domain für ASOL.PROJECTS
|
||||
password : null # das Passwort
|
||||
}
|
||||
dbserver : {
|
||||
server : "some-server",
|
||||
|
|
|
@ -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
|
||||
]
|
|
@ -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()
|
|
@ -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");
|
||||
|
||||
|
||||
|
|
@ -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")
|
|
@ -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")
|
|
@ -48,6 +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("dbenv/dbenv.asmx")
|
||||
print("WEB Environment:", client.service.getEnvironment())
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(applus_configs.serverConfYamlTest)
|
||||
|
|
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||
|
||||
[project]
|
||||
name = "PyAPplus64"
|
||||
version = "1.0.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]
|
||||
|
|
|
@ -7,12 +7,14 @@
|
|||
# https://opensource.org/licenses/MIT.
|
||||
|
||||
from . import applus_db
|
||||
from . import applus_job
|
||||
from . import applus_server
|
||||
from . import applus_sysconf
|
||||
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
|
||||
|
@ -35,21 +37,16 @@ class APplusServer:
|
|||
"""
|
||||
def __init__(self,
|
||||
db_settings: applus_db.APplusDBSettings,
|
||||
server_settings: applus_server.APplusAppServerSettings,
|
||||
web_settings: applus_server.APplusWebServerSettings):
|
||||
server_settings: applus_server.APplusServerSettings):
|
||||
|
||||
self.db_settings: applus_db.APplusDBSettings = db_settings
|
||||
"""Die Einstellungen für die Datenbankverbindung"""
|
||||
|
||||
self.web_settings: applus_server.APplusWebServerSettings = web_settings
|
||||
"""Die Einstellungen für die Datenbankverbindung"""
|
||||
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"""
|
||||
|
@ -57,19 +54,67 @@ class APplusServer:
|
|||
self.sysconf: applus_sysconf.APplusSysConf = applus_sysconf.APplusSysConf(self)
|
||||
"""erlaubt den Zugriff auf die Sysconfig"""
|
||||
|
||||
self.job: applus_job.APplusJob = applus_job.APplusJob(self)
|
||||
"""erlaubt Arbeiten mit Jobs"""
|
||||
|
||||
self.scripttool: applus_scripttool.APplusScriptTool = applus_scripttool.APplusScriptTool(self)
|
||||
"""erlaubt den einfachen Zugriff auf Funktionen des ScriptTools"""
|
||||
|
||||
self.client_table = self.server_conn.getClient("p2core", "Table")
|
||||
self.client_xml = self.server_conn.getClient("p2core", "XML")
|
||||
self.client_nummer = self.server_conn.getClient("p2system", "Nummer")
|
||||
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:
|
||||
"""
|
||||
|
@ -92,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."""
|
||||
|
@ -102,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.
|
||||
|
@ -122,7 +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)
|
||||
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"""
|
||||
|
@ -130,9 +198,9 @@ class APplusServer:
|
|||
c = self.dbQuerySingleValue(sql, table)
|
||||
return (c > 0)
|
||||
|
||||
def getClient(self, package: str, name: str) -> Client:
|
||||
"""Erzeugt einen zeep - Client.
|
||||
Mittels dieses Clients kann die WSDL Schnittstelle angesprochen werden.
|
||||
def getAppClient(self, package: str, name: str) -> Client:
|
||||
"""Erzeugt einen zeep - Client für den APP-Server.
|
||||
Mittels dieses Clients kann eines WSDL Schnittstelle des APP-Servers angesprochen werden.
|
||||
Wird als *package* "p2core" und als *name* "Table" verwendet und der
|
||||
resultierende client "client" genannt, dann kann
|
||||
z.B. mittels "client.service.getCompleteSQL(sql)" vom AppServer ein Vervollständigen
|
||||
|
@ -145,7 +213,22 @@ class APplusServer:
|
|||
:return: den Client
|
||||
:rtype: Client
|
||||
"""
|
||||
return self.server_conn.getClient(package, name)
|
||||
return self.server_conn.getAppClient(package, name)
|
||||
|
||||
def getWebClient(self, url: str) -> Client:
|
||||
"""Erzeugt einen zeep - Client für den Web-Server.
|
||||
Mittels dieses Clients kann die von einer ASMX-Seite zur Verfügung gestellte Schnittstelle angesprochen werden.
|
||||
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
|
||||
:rtype: Client
|
||||
"""
|
||||
return self.server_conn.getWebClient(url)
|
||||
|
||||
def getTableFields(self, table: str, isComputed: Optional[bool] = None) -> Set[str]:
|
||||
"""
|
||||
|
@ -173,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."""
|
||||
|
@ -219,11 +305,56 @@ class APplusServer:
|
|||
"""
|
||||
return self.client_nummer.service.nextNumber(obj)
|
||||
|
||||
def updateDatabase(self, file : str) -> str:
|
||||
"""
|
||||
Führt eine DBAnpass-xml Datei aus.
|
||||
:param file: DB-Anpass Datei, die ausgeführt werden soll
|
||||
:type file: string
|
||||
:return: Infos zur Ausführung
|
||||
:rtype: str
|
||||
"""
|
||||
jobId = self.job.createSOAPJob("run DBAnpass " + file);
|
||||
self.client_adaptdb.service.updateDatabase(jobId, "de", file);
|
||||
res = self.job.getResultURLString(jobId)
|
||||
if res is None: res = "FEHLER";
|
||||
if (res == "Folgende Befehle konnten nicht ausgeführt werden:\n\n"):
|
||||
return ""
|
||||
else:
|
||||
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.web_settings.baseurl:
|
||||
if not self.server_settings.webserver:
|
||||
raise Exception("keine Webserver-BaseURL gesetzt")
|
||||
|
||||
url = str(self.web_settings.baseurl) + base
|
||||
url = str(self.server_settings.webserver) + base
|
||||
firstArg = True
|
||||
for arg, argv in kwargs.items():
|
||||
if not (argv is None):
|
||||
|
@ -244,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"""
|
||||
|
@ -251,20 +390,21 @@ def applusFromConfigDict(yamlDict: Dict[str, Any], user: Optional[str] = None, e
|
|||
user = yamlDict["appserver"]["user"]
|
||||
if env is None or env == '':
|
||||
env = yamlDict["appserver"]["env"]
|
||||
app_server = applus_server.APplusAppServerSettings(
|
||||
server_settings = applus_server.APplusServerSettings(
|
||||
webserver=yamlDict.get("webserver", {}).get("baseurl", None),
|
||||
appserver=yamlDict["appserver"]["server"],
|
||||
appserverPort=yamlDict["appserver"]["port"],
|
||||
user=user, # type: ignore
|
||||
env=env)
|
||||
web_server = applus_server.APplusWebServerSettings(
|
||||
baseurl=yamlDict.get("webserver", {}).get("baseurl", None)
|
||||
)
|
||||
env=env,
|
||||
webserverUser=yamlDict.get("webserver", {}).get("user", None),
|
||||
webserverUserDomain=yamlDict.get("webserver", {}).get("userDomain", None),
|
||||
webserverPassword=yamlDict.get("webserver", {}).get("password", None))
|
||||
dbparams = applus_db.APplusDBSettings(
|
||||
server=yamlDict["dbserver"]["server"],
|
||||
database=yamlDict["dbserver"]["db"],
|
||||
user=yamlDict["dbserver"]["user"],
|
||||
password=yamlDict["dbserver"]["password"])
|
||||
return APplusServer(dbparams, app_server, web_server)
|
||||
return APplusServer(dbparams, server_settings)
|
||||
|
||||
|
||||
def applusFromConfigFile(yamlfile: 'FileDescriptorOrPath',
|
||||
|
@ -281,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)
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -108,6 +108,11 @@ def rawQuerySingleValue(cnxn: pyodbc.Connection, sql: SqlStatement, *args: Any)
|
|||
else:
|
||||
return None
|
||||
|
||||
def rawExecute(cnxn: pyodbc.Connection, sql: SqlStatement, *args: Any) -> Any:
|
||||
"""Führt ein SQL Statement direkt aus"""
|
||||
_logSQLWithArgs(sql, *args)
|
||||
with cnxn.cursor() as cursor:
|
||||
return cursor.execute(str(sql), *args)
|
||||
|
||||
def getUniqueFieldsOfTable(cnxn: pyodbc.Connection, table: str) -> Dict[str, List[str]]:
|
||||
"""
|
||||
|
|
|
@ -0,0 +1,203 @@
|
|||
# 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.
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from zeep import Client
|
||||
import uuid
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .applus import APplusServer
|
||||
|
||||
|
||||
class APplusJob:
|
||||
"""
|
||||
Zugriff auf Jobs
|
||||
|
||||
:param server: die Verbindung zum Server
|
||||
:type server: APplusServer
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, server: 'APplusServer') -> None:
|
||||
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:
|
||||
"""
|
||||
Erzeugt einen neuen SOAP Job mit der gegebenen Bezeichnung und liefert die neue JobID.
|
||||
:param bez: die Bezeichnung des neuen Jobs
|
||||
:type bez: str
|
||||
:return: die neue JobID
|
||||
:rtype: str
|
||||
"""
|
||||
jobId = str(uuid.uuid4())
|
||||
self.client.service.create(jobId, "SOAP", "0", "about:soapcall", bez)
|
||||
return jobId
|
||||
|
||||
def restart(self, jobId: str) -> str:
|
||||
"""
|
||||
Startet einen Job neu
|
||||
:param jobId: die ID des Jobs
|
||||
:type jobId: str
|
||||
:return: die URL des Jobs
|
||||
:rtype: str
|
||||
"""
|
||||
return self.client.service.restart(jobId)
|
||||
|
||||
def setResultURL(self, jobId: str, resurl: str) -> None:
|
||||
"""
|
||||
Setzt die ResultURL eines Jobs
|
||||
:param jobId: die ID des Jobs
|
||||
:type jobId: str
|
||||
:param resurl: die neue Result-URL
|
||||
:type resurl: str
|
||||
"""
|
||||
self.client.service.setResultURL(jobId, resurl)
|
||||
|
||||
def getResultURL(self, jobId: str) -> str:
|
||||
"""
|
||||
Liefert die ResultURL eines Jobs
|
||||
:param jobId: die ID des Jobs
|
||||
:type jobId: str
|
||||
:return: die Result-URL
|
||||
:rtype: str
|
||||
"""
|
||||
return self.client.service.getResultURL(jobId)
|
||||
|
||||
def getResultURLString(self, jobId: str) -> Optional[str]:
|
||||
"""
|
||||
Liefert die ResultURL eines Jobs, wobei ein evtl. Präfix "retstring://" entfernt wird und
|
||||
alle anderen Werte durch None ersetzt werden.
|
||||
:param jobId: die ID des Jobs
|
||||
:type jobId: str
|
||||
:return: die Result-URL als String
|
||||
:rtype: str
|
||||
"""
|
||||
res = self.getResultURL(jobId)
|
||||
if res is None:
|
||||
return None
|
||||
|
||||
if res.startswith("retstring://"):
|
||||
return res[12:]
|
||||
return None
|
||||
|
||||
def setPtURL(self, jobId: str, pturl: str) -> None:
|
||||
"""
|
||||
Setzt die ResultURL eines Jobs
|
||||
:param jobId: die ID des Jobs
|
||||
:type jobId: str
|
||||
:param pturl: die neue PtURL
|
||||
:type pturl: str
|
||||
"""
|
||||
self.client.service.setPtURL(jobId, pturl)
|
||||
|
||||
def getPtURL(self, jobId: str) -> str:
|
||||
"""
|
||||
Liefert die PtURL eines Jobs
|
||||
:param jobId: die ID des Jobs
|
||||
:type jobId: str
|
||||
:return: die Pt-URL
|
||||
:rtype: str
|
||||
"""
|
||||
return self.client.service.getPtURL(jobId)
|
||||
|
||||
def setResult(self, jobId: str, res: str) -> None:
|
||||
"""
|
||||
Setzt das Result eines Jobs
|
||||
:param jobId: die ID des Jobs
|
||||
:type jobId: str
|
||||
:param res: das neue Result
|
||||
:type res: str
|
||||
"""
|
||||
self.client.service.setResult(jobId, res)
|
||||
|
||||
def getResult(self, jobId: str) -> str:
|
||||
"""
|
||||
Liefert das Result eines Jobs
|
||||
:param jobId: die ID des Jobs
|
||||
:type jobId: str
|
||||
:return: das Result
|
||||
:rtype: str
|
||||
"""
|
||||
return self.client.service.getResult(jobId)
|
||||
|
||||
def setInfo(self, jobId: str, info: str) -> bool:
|
||||
"""
|
||||
Setzt die Informationen zu dem Job
|
||||
:param jobId: die ID des Jobs
|
||||
:type jobId: str
|
||||
:param info: die neuen Infos
|
||||
:type info: str
|
||||
"""
|
||||
return self.client.service.setInfo(jobId, info)
|
||||
|
||||
def getInfo(self, jobId: str) -> str:
|
||||
"""
|
||||
Liefert die Info eines Jobs
|
||||
:param jobId: die ID des Jobs
|
||||
:type jobId: str
|
||||
:return: die Info
|
||||
:rtype: str
|
||||
"""
|
||||
return self.client.service.getInfo(jobId)
|
||||
|
||||
def getStatus(self, jobId: str) -> str:
|
||||
"""
|
||||
Liefert Informationen zum Job
|
||||
:param jobId: die ID des Jobs
|
||||
:type jobId: str
|
||||
:return: die Infos
|
||||
:rtype: str
|
||||
"""
|
||||
return self.client.service.getStatus(jobId)
|
||||
|
||||
def setPosition(self, jobId: str, pos: int, max: int) -> bool:
|
||||
"""
|
||||
Schrittfunktion
|
||||
:param jobId: die ID des Jobs
|
||||
:type jobId: str
|
||||
:param pos: Position
|
||||
:type pos: int
|
||||
:param max: Anzahl Schritte in Anzeige
|
||||
:type max: int
|
||||
"""
|
||||
return self.client.service.setPosition(jobId, pos, max)
|
||||
|
||||
def start(self, jobId: str) -> bool:
|
||||
"""
|
||||
Startet einen Job
|
||||
:param jobId: die ID des Jobs
|
||||
:type jobId: str
|
||||
"""
|
||||
return self.client.service.start(jobId)
|
||||
|
||||
def kill(self, jobId: str) -> None:
|
||||
"""
|
||||
Startet einen Job
|
||||
:param jobId: die ID des Jobs
|
||||
:type jobId: str
|
||||
"""
|
||||
self.client.service.start(jobId)
|
||||
|
||||
def finish(self, jobId: str, status: int, resurl: str) -> bool:
|
||||
"""
|
||||
Beendet den Job
|
||||
:param jobId: die ID des Jobs
|
||||
:type jobId: str
|
||||
:param status: der Status 2 (OK), 3 (Fehler)
|
||||
:type status: int
|
||||
:param resurl: die neue resurl des Jobs
|
||||
:type resurl: str
|
||||
"""
|
||||
return self.client.service.finish(jobId, status, resurl)
|
|
@ -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.getClient("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
|
||||
|
||||
|
|
|
@ -13,56 +13,66 @@ from zeep.transports import Transport
|
|||
from zeep.cache import SqliteCache
|
||||
from typing import Optional, Dict
|
||||
|
||||
try:
|
||||
from requests_negotiate_sspi import HttpNegotiateAuth
|
||||
auth_negotiate_present = True
|
||||
except:
|
||||
auth_negotiate_present = False
|
||||
|
||||
class APplusAppServerSettings:
|
||||
class APplusServerSettings:
|
||||
"""
|
||||
Einstellungen, mit welchem APplus App-Server sich verbunden werden soll.
|
||||
Einstellungen, mit welchem APplus App- and Web-Server sich verbunden werden soll.
|
||||
"""
|
||||
|
||||
def __init__(self, appserver: str, appserverPort: int, user: str, env: Optional[str] = None):
|
||||
def __init__(self, webserver: str, appserver: str, appserverPort: int, user: str, env: Optional[str] = None, webserverUser : Optional[str] = None, webserverUserDomain : Optional[str] = None, webserverPassword : Optional[str] = None):
|
||||
self.appserver = appserver
|
||||
self.appserverPort = appserverPort
|
||||
self.user = user
|
||||
self.env = env
|
||||
|
||||
|
||||
class APplusWebServerSettings:
|
||||
"""
|
||||
Einstellungen, mit welchem APplus Web-Server sich verbunden werden soll.
|
||||
"""
|
||||
|
||||
def __init__(self, baseurl: Optional[str] = None):
|
||||
self.baseurl: Optional[str] = baseurl
|
||||
self.webserver = webserver
|
||||
self.webserverUser = webserverUser
|
||||
self.webserverUserDomain = webserverUserDomain
|
||||
self.webserverPassword = webserverPassword
|
||||
try:
|
||||
assert (isinstance(self.baseurl, str))
|
||||
if not (self.baseurl is None) and not (self.baseurl[-1] == "/"):
|
||||
self.baseurl = self.baseurl + "/"
|
||||
if not (self.webserver[-1] == "/"):
|
||||
self.webserver = self.webserver + "/"
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
class APplusServerConnection:
|
||||
"""Verbindung zu einem APplus APP-Server
|
||||
"""Verbindung zu einem APplus APP- und Web-Server
|
||||
|
||||
:param settings: die Einstellungen für die Verbindung mit dem APplus Server
|
||||
:type settings: APplusAppServerSettings
|
||||
"""
|
||||
def __init__(self, settings: APplusAppServerSettings) -> None:
|
||||
def __init__(self, settings: APplusServerSettings) -> None:
|
||||
userEnv = settings.user
|
||||
if (settings.env):
|
||||
userEnv += "|" + settings.env
|
||||
|
||||
session = Session()
|
||||
session.auth = HTTPBasicAuth(userEnv, "")
|
||||
sessionApp = Session()
|
||||
sessionApp.auth = HTTPBasicAuth(userEnv, "")
|
||||
|
||||
self.transportApp = Transport(cache=SqliteCache(), session=sessionApp)
|
||||
# self.transportApp = Transport(session=sessionApp)
|
||||
|
||||
if auth_negotiate_present:
|
||||
sessionWeb = Session()
|
||||
sessionWeb.auth = HttpNegotiateAuth(username=settings.webserverUser, password=settings.webserverPassword, domain=settings.webserverUserDomain)
|
||||
|
||||
self.transportWeb = Transport(cache=SqliteCache(), session=sessionWeb)
|
||||
# self.transportWeb = Transport(session=sessionWeb)
|
||||
else:
|
||||
self.transportWeb = self.transportApp # führt vermutlich zu Authorization-Fehlern, diese sind aber zumindest hilfreicher als NULL-Pointer Exceptions
|
||||
|
||||
self.transport = Transport(cache=SqliteCache(), session=session)
|
||||
# self.transport = Transport(session=session)
|
||||
self.clientCache: Dict[str, Client] = {}
|
||||
self.settings = settings
|
||||
self.appserverUrl = "http://" + settings.appserver + ":" + str(settings.appserverPort) + "/"
|
||||
|
||||
def getClient(self, package: str, name: str) -> Client:
|
||||
"""Erzeugt einen zeep - Client.
|
||||
def getAppClient(self, package: str, name: str) -> Client:
|
||||
"""Erzeugt einen zeep - Client für den APP-Server.
|
||||
Mittels dieses Clients kann die WSDL Schnittstelle angesprochen werden.
|
||||
Wird als *package* "p2core" und als *name* "Table" verwendet und der
|
||||
resultierende client "client" genannt, dann kann
|
||||
|
@ -76,11 +86,36 @@ class APplusServerConnection:
|
|||
:return: den Client
|
||||
:rtype: Client
|
||||
"""
|
||||
url = package+"/"+name
|
||||
cacheKey = "APP:"+package+"/"+name
|
||||
try:
|
||||
return self.clientCache[url]
|
||||
return self.clientCache[cacheKey]
|
||||
except:
|
||||
fullClientUrl = self.appserverUrl + url + ".jws?wsdl"
|
||||
client = Client(fullClientUrl, transport=self.transport)
|
||||
self.clientCache[url] = client
|
||||
fullClientUrl = self.appserverUrl + package+"/"+name + ".jws?wsdl"
|
||||
client = Client(fullClientUrl, transport=self.transportApp)
|
||||
self.clientCache[cacheKey] = client
|
||||
return client
|
||||
|
||||
def getWebClient(self, url: str) -> Client:
|
||||
"""Erzeugt einen zeep - Client für den Web-Server.
|
||||
Mittels dieses Clients kann die von einer ASMX-Seite zur Verfügung gestellte Schnittstelle angesprochen werden.
|
||||
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
|
||||
:rtype: Client
|
||||
"""
|
||||
if not auth_negotiate_present:
|
||||
raise Exception("getWebClient ist nicht verfügbar, da Python-Package requests-negotiate-sspi nicht gefunden wurde")
|
||||
|
||||
cacheKey = "WEB:"+url
|
||||
try:
|
||||
return self.clientCache[cacheKey]
|
||||
except:
|
||||
fullClientUrl = self.settings.webserver + url + "?wsdl"
|
||||
client = Client(fullClientUrl, transport=self.transportWeb)
|
||||
self.clientCache[cacheKey] = client
|
||||
return client
|
||||
|
|
|
@ -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.getClient("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 = {}
|
||||
|
|
|
@ -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)
|
||||
|
@ -392,7 +393,7 @@ class SqlConditionBinComp(SqlConditionPrepared):
|
|||
:type value2: SqlValue
|
||||
"""
|
||||
def __init__(self, op: str, value1: SqlValue, value2: SqlValue):
|
||||
if not value1 or not value2:
|
||||
if value1 is None or value2 is None:
|
||||
raise Exception("SqlConditionBinComp: value not provided")
|
||||
|
||||
cond = "({} {} {})".format(formatSqlValue(value1), op, formatSqlValue(value2))
|
||||
|
|
Loading…
Reference in New Issue