Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
e7fe3fb037 | |||
599339a270 |
10
Changelog.md
10
Changelog.md
@ -1,5 +1,15 @@
|
|||||||
# Changelog
|
# 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
|
## 06.05.2023 v1.0.1
|
||||||
- Code-Cleanup mit Hilfe von flake8
|
- Code-Cleanup mit Hilfe von flake8
|
||||||
- Bugfix: neue Python 3.10 Syntax entfernt
|
- Bugfix: neue Python 3.10 Syntax entfernt
|
||||||
|
28
README.md
28
README.md
@ -1,21 +1,21 @@
|
|||||||
# PyAPplus64
|
# PyAPplus64
|
||||||
|
|
||||||
## Beschreibung
|
## Beschreibung
|
||||||
Das Paket `PyAPplus64` enthält eine Sammlung von Python Tools für die Interaktion mit dem ERP-System APplus 6.4.
|
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.
|
Es sollte auch für andere APplus Versionen nützlich sein.
|
||||||
|
|
||||||
Zielgruppe sind APplus-Administratoren und Anpassungs-Entwickler. Die Tools erlauben u.a.
|
Zielgruppe sind APplus-Administratoren und Anpassungs-Entwickler. Die Tools erlauben u.a.
|
||||||
|
|
||||||
- einfacher Zugriff auf SOAP-Schnittstelle des App-Servers
|
- einfacher Zugriff auf SOAP-Schnittstelle des App-Servers
|
||||||
+ damit Zugriff auf SysConfig
|
+ damit Zugriff auf SysConfig
|
||||||
+ Zugriff auf Tools `nextNumber` für Erzeugung der nächsten Nummer für ein Business-Object
|
+ 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
|
- 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
|
+ 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
|
+ 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
|
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
|
zum direkten Zugriff über die Datenbank werden dabei evtl. zusätzliche
|
||||||
Checks ausgeführt, bestimmte Felder automatisch gesetzt oder bestimmte Aktionen angestoßen.
|
Checks ausgeführt, bestimmte Felder automatisch gesetzt oder bestimmte Aktionen angestoßen.
|
||||||
- das Duplizieren von Datensätzen
|
- das Duplizieren von Datensätzen
|
||||||
+ zu kopierende Felder aus XML-Definitionen werden ausgewertet
|
+ zu kopierende Felder aus XML-Definitionen werden ausgewertet
|
||||||
+ Abhängige Objekte können einfach ebenfalls mit-kopiert werden
|
+ Abhängige Objekte können einfach ebenfalls mit-kopiert werden
|
||||||
@ -35,9 +35,9 @@ aus, dass im Laufe der Zeit weitere Features hinzukommen.
|
|||||||
|
|
||||||
## Warnung
|
## Warnung
|
||||||
|
|
||||||
`PyAPplus64` erlaubt den schreibenden Zugriff auf die APplus Datenbank und beliebige
|
`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
|
Aufrufe von SOAP-Methoden. Unsachgemäße Nutzung kann Ihre Daten zerstören. Benutzen Sie
|
||||||
`PyAPplus64` daher bitte vorsichtig.
|
`PyAPplus64` daher bitte vorsichtig.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@ -47,6 +47,14 @@ PyAPplus64 wurde auf PyPi veröffentlicht. Es lässt sich daher einfach mittel `
|
|||||||
pip install PyAPplus64
|
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
|
## Links
|
||||||
|
|
||||||
- [PyPi](https://pypi.org/project/PyAPplus64/)
|
- [PyPi](https://pypi.org/project/PyAPplus64/)
|
||||||
|
@ -3,8 +3,8 @@ Abhängigkeiten
|
|||||||
|
|
||||||
pyodbc
|
pyodbc
|
||||||
------
|
------
|
||||||
Für die Datenbankverbindung wird ``pyodbc`` (``python -m pip install pyodbc``) verwendet.
|
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.
|
Der passende ODBC Treiber, MS SQL Server 2012 Native Client, wird zusätzlich benötigt.
|
||||||
Dieser kann von Microsoft bezogen werden.
|
Dieser kann von Microsoft bezogen werden.
|
||||||
|
|
||||||
|
|
||||||
@ -13,18 +13,25 @@ zeep
|
|||||||
Die Soap-Library ``zeep`` wird benutzt (``python -m pip install 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
|
PyYaml
|
||||||
------
|
------
|
||||||
|
|
||||||
Die Library ``pyyaml`` wird für Config-Dateien benutzt (``python -m pip install pyyaml``).
|
Die Library ``pyyaml`` wird für Config-Dateien benutzt (``python -m pip install pyyaml``).
|
||||||
|
|
||||||
|
|
||||||
Sphinx
|
Sphinx
|
||||||
------
|
------
|
||||||
Diese Dokumentation ist mit Sphinx geschrieben.
|
Diese Dokumentation ist mit Sphinx geschrieben.
|
||||||
``python -m pip install sphinx``. Dokumentation ist im Unterverzeichnis
|
``python -m pip install sphinx``. Dokumentation ist im Unterverzeichnis
|
||||||
`docs` zu finden. Sie kann mittels ``make.bat html`` erzeugt werden,
|
`docs` zu finden. Sie kann mittels ``make.bat html`` erzeugt werden,
|
||||||
dies ruft intern ``sphinx-build -M html source build`` auf. Die Dokumentation
|
dies ruft intern ``sphinx-build -M html source build`` auf. Die Dokumentation
|
||||||
der Python-API sollte evtl. vorher
|
der Python-API sollte evtl. vorher
|
||||||
mittels ``sphinx-apidoc -T -f ../src/PyAPplus64 -o source/generated`` erzeugt
|
mittels ``sphinx-apidoc -T -f ../src/PyAPplus64 -o source/generated`` erzeugt
|
||||||
oder aktualisiert werden. Evtl. können 2 Aufrufe von ``make.bat html`` sinnvoll
|
oder aktualisiert werden. Evtl. können 2 Aufrufe von ``make.bat html`` sinnvoll
|
||||||
@ -36,5 +43,6 @@ Die erzeugte Doku findet sich im Verzeichnis ``build/html``.
|
|||||||
|
|
||||||
Pandas / SqlAlchemy / xlsxwriter
|
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`).
|
||||||
|
|
||||||
|
@ -85,10 +85,10 @@ Zugriff auf die Sysconf möglich::
|
|||||||
print (server.sysconf.getList("STAMM", "EULAENDER"))
|
print (server.sysconf.getList("STAMM", "EULAENDER"))
|
||||||
|
|
||||||
Dank der Bibliothek `zeep` ist es auch sehr einfach möglich, auf beliebige SOAP-Methoden zuzugreifen.
|
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,
|
Beispielsweise kann auf die Sys-Config auch händisch, d.h. durch direkten Aufruf einer SOAP-Methode
|
||||||
zugegriffen werden::
|
des APP-Servers zugegriffen werden::
|
||||||
|
|
||||||
client = server.server_conn.getClient("p2system", "SysConf");
|
client = server.server_conn.getAppClient("p2system", "SysConf");
|
||||||
print (client.service.getString("STAMM", "MYLAND"))
|
print (client.service.getString("STAMM", "MYLAND"))
|
||||||
|
|
||||||
|
|
||||||
|
@ -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.0.1'
|
version = '1.1.1'
|
||||||
release = '1.0.1'
|
release = '1.1.1'
|
||||||
|
|
||||||
# -- 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
|
||||||
|
@ -20,6 +20,14 @@ PyAPplus64.applus\_db module
|
|||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
PyAPplus64.applus\_job module
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
.. automodule:: PyAPplus64.applus_job
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
PyAPplus64.applus\_scripttool module
|
PyAPplus64.applus\_scripttool module
|
||||||
------------------------------------
|
------------------------------------
|
||||||
|
|
||||||
|
@ -16,7 +16,10 @@ appserver : {
|
|||||||
env : "default-umgebung" # hier wirklich Umgebung, nicht Mandant verwenden
|
env : "default-umgebung" # hier wirklich Umgebung, nicht Mandant verwenden
|
||||||
}
|
}
|
||||||
webserver : {
|
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 : {
|
dbserver : {
|
||||||
server : "some-server",
|
server : "some-server",
|
||||||
|
@ -48,6 +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")
|
||||||
|
print("ARTIKEL-ASMX Date:", client.service.getServerDate())
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main(applus_configs.serverConfYamlTest)
|
main(applus_configs.serverConfYamlTest)
|
||||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "PyAPplus64"
|
name = "PyAPplus64"
|
||||||
version = "1.0.1"
|
version = "1.1.1"
|
||||||
authors = [
|
authors = [
|
||||||
{ name="Thomas Tuerk", email="kontakt@thomas-tuerk.de" },
|
{ name="Thomas Tuerk", email="kontakt@thomas-tuerk.de" },
|
||||||
]
|
]
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
# https://opensource.org/licenses/MIT.
|
# https://opensource.org/licenses/MIT.
|
||||||
|
|
||||||
from . import applus_db
|
from . import applus_db
|
||||||
|
from . import applus_job
|
||||||
from . import applus_server
|
from . import applus_server
|
||||||
from . import applus_sysconf
|
from . import applus_sysconf
|
||||||
from . import applus_scripttool
|
from . import applus_scripttool
|
||||||
@ -35,14 +36,13 @@ class APplusServer:
|
|||||||
"""
|
"""
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
db_settings: applus_db.APplusDBSettings,
|
db_settings: applus_db.APplusDBSettings,
|
||||||
server_settings: applus_server.APplusAppServerSettings,
|
server_settings: applus_server.APplusServerSettings):
|
||||||
web_settings: applus_server.APplusWebServerSettings):
|
|
||||||
|
|
||||||
self.db_settings: applus_db.APplusDBSettings = db_settings
|
self.db_settings: applus_db.APplusDBSettings = db_settings
|
||||||
"""Die Einstellungen für die Datenbankverbindung"""
|
"""Die Einstellungen für die Datenbankverbindung"""
|
||||||
|
|
||||||
self.web_settings: applus_server.APplusWebServerSettings = web_settings
|
self.server_settings : applus_server.APplusServerSettings = server_settings
|
||||||
"""Die Einstellungen für die Datenbankverbindung"""
|
"""Einstellung für die Verbindung zum APP- und Webserver"""
|
||||||
|
|
||||||
self.db_conn = db_settings.connect()
|
self.db_conn = db_settings.connect()
|
||||||
"""
|
"""
|
||||||
@ -57,12 +57,17 @@ class APplusServer:
|
|||||||
self.sysconf: applus_sysconf.APplusSysConf = applus_sysconf.APplusSysConf(self)
|
self.sysconf: applus_sysconf.APplusSysConf = applus_sysconf.APplusSysConf(self)
|
||||||
"""erlaubt den Zugriff auf die Sysconfig"""
|
"""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)
|
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.getClient("p2core", "Table")
|
self.client_table = self.server_conn.getAppClient("p2core", "Table")
|
||||||
self.client_xml = self.server_conn.getClient("p2core", "XML")
|
self.client_xml = self.server_conn.getAppClient("p2core", "XML")
|
||||||
self.client_nummer = self.server_conn.getClient("p2system", "Nummer")
|
self.client_nummer = self.server_conn.getAppClient("p2system", "Nummer")
|
||||||
|
self.client_adaptdb = self.server_conn.getAppClient("p2dbtools", "AdaptDB");
|
||||||
|
|
||||||
|
|
||||||
def reconnectDB(self) -> None:
|
def reconnectDB(self) -> None:
|
||||||
try:
|
try:
|
||||||
@ -124,15 +129,21 @@ class APplusServer:
|
|||||||
sqlC = self.completeSQL(sql, raw=raw)
|
sqlC = self.completeSQL(sql, raw=raw)
|
||||||
return applus_db.rawQuerySingleValue(self.db_conn, sqlC, *args)
|
return applus_db.rawQuerySingleValue(self.db_conn, sqlC, *args)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
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"""
|
||||||
sql = "select count(*) from SYS.TABLES T where T.NAME=?"
|
sql = "select count(*) from SYS.TABLES T where T.NAME=?"
|
||||||
c = self.dbQuerySingleValue(sql, table)
|
c = self.dbQuerySingleValue(sql, table)
|
||||||
return (c > 0)
|
return (c > 0)
|
||||||
|
|
||||||
def getClient(self, package: str, name: str) -> Client:
|
def getAppClient(self, package: str, name: str) -> Client:
|
||||||
"""Erzeugt einen zeep - Client.
|
"""Erzeugt einen zeep - Client für den APP-Server.
|
||||||
Mittels dieses Clients kann die WSDL Schnittstelle angesprochen werden.
|
Mittels dieses Clients kann eines WSDL Schnittstelle des APP-Servers angesprochen werden.
|
||||||
Wird als *package* "p2core" und als *name* "Table" verwendet und der
|
Wird als *package* "p2core" und als *name* "Table" verwendet und der
|
||||||
resultierende client "client" genannt, dann kann
|
resultierende client "client" genannt, dann kann
|
||||||
z.B. mittels "client.service.getCompleteSQL(sql)" vom AppServer ein Vervollständigen
|
z.B. mittels "client.service.getCompleteSQL(sql)" vom AppServer ein Vervollständigen
|
||||||
@ -145,7 +156,20 @@ class APplusServer:
|
|||||||
:return: den Client
|
:return: den Client
|
||||||
:rtype: 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".
|
||||||
|
|
||||||
|
: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]:
|
def getTableFields(self, table: str, isComputed: Optional[bool] = None) -> Set[str]:
|
||||||
"""
|
"""
|
||||||
@ -219,11 +243,29 @@ class APplusServer:
|
|||||||
"""
|
"""
|
||||||
return self.client_nummer.service.nextNumber(obj)
|
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 makeWebLink(self, base: str, **kwargs: Any) -> str:
|
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")
|
raise Exception("keine Webserver-BaseURL gesetzt")
|
||||||
|
|
||||||
url = str(self.web_settings.baseurl) + base
|
url = str(self.server_settings.webserver) + base
|
||||||
firstArg = True
|
firstArg = True
|
||||||
for arg, argv in kwargs.items():
|
for arg, argv in kwargs.items():
|
||||||
if not (argv is None):
|
if not (argv is None):
|
||||||
@ -251,20 +293,21 @@ def applusFromConfigDict(yamlDict: Dict[str, Any], user: Optional[str] = None, e
|
|||||||
user = yamlDict["appserver"]["user"]
|
user = yamlDict["appserver"]["user"]
|
||||||
if env is None or env == '':
|
if env is None or env == '':
|
||||||
env = yamlDict["appserver"]["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"],
|
appserver=yamlDict["appserver"]["server"],
|
||||||
appserverPort=yamlDict["appserver"]["port"],
|
appserverPort=yamlDict["appserver"]["port"],
|
||||||
user=user, # type: ignore
|
user=user, # type: ignore
|
||||||
env=env)
|
env=env,
|
||||||
web_server = applus_server.APplusWebServerSettings(
|
webserverUser=yamlDict.get("webserver", {}).get("user", None),
|
||||||
baseurl=yamlDict.get("webserver", {}).get("baseurl", None)
|
webserverUserDomain=yamlDict.get("webserver", {}).get("userDomain", None),
|
||||||
)
|
webserverPassword=yamlDict.get("webserver", {}).get("password", None))
|
||||||
dbparams = applus_db.APplusDBSettings(
|
dbparams = applus_db.APplusDBSettings(
|
||||||
server=yamlDict["dbserver"]["server"],
|
server=yamlDict["dbserver"]["server"],
|
||||||
database=yamlDict["dbserver"]["db"],
|
database=yamlDict["dbserver"]["db"],
|
||||||
user=yamlDict["dbserver"]["user"],
|
user=yamlDict["dbserver"]["user"],
|
||||||
password=yamlDict["dbserver"]["password"])
|
password=yamlDict["dbserver"]["password"])
|
||||||
return APplusServer(dbparams, app_server, web_server)
|
return APplusServer(dbparams, server_settings)
|
||||||
|
|
||||||
|
|
||||||
def applusFromConfigFile(yamlfile: 'FileDescriptorOrPath',
|
def applusFromConfigFile(yamlfile: 'FileDescriptorOrPath',
|
||||||
|
@ -108,6 +108,11 @@ def rawQuerySingleValue(cnxn: pyodbc.Connection, sql: SqlStatement, *args: Any)
|
|||||||
else:
|
else:
|
||||||
return None
|
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]]:
|
def getUniqueFieldsOfTable(cnxn: pyodbc.Connection, table: str) -> Dict[str, List[str]]:
|
||||||
"""
|
"""
|
||||||
|
195
src/PyAPplus64/applus_job.py
Normal file
195
src/PyAPplus64/applus_job.py
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
# 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
|
||||||
|
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.client = server.getAppClient("p2core", "Job")
|
||||||
|
|
||||||
|
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)
|
@ -57,7 +57,7 @@ class APplusScriptTool:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, server: APplusServer) -> None:
|
def __init__(self, server: APplusServer) -> None:
|
||||||
self.client = server.getClient("p2script", "ScriptTool")
|
self.client = server.getAppClient("p2script", "ScriptTool")
|
||||||
|
|
||||||
def getCurrentDate(self) -> str:
|
def getCurrentDate(self) -> str:
|
||||||
return self.client.service.getCurrentDate()
|
return self.client.service.getCurrentDate()
|
||||||
|
@ -13,56 +13,66 @@ from zeep.transports import Transport
|
|||||||
from zeep.cache import SqliteCache
|
from zeep.cache import SqliteCache
|
||||||
from typing import Optional, Dict
|
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.appserver = appserver
|
||||||
self.appserverPort = appserverPort
|
self.appserverPort = appserverPort
|
||||||
self.user = user
|
self.user = user
|
||||||
self.env = env
|
self.env = env
|
||||||
|
|
||||||
|
self.webserver = webserver
|
||||||
class APplusWebServerSettings:
|
self.webserverUser = webserverUser
|
||||||
"""
|
self.webserverUserDomain = webserverUserDomain
|
||||||
Einstellungen, mit welchem APplus Web-Server sich verbunden werden soll.
|
self.webserverPassword = webserverPassword
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, baseurl: Optional[str] = None):
|
|
||||||
self.baseurl: Optional[str] = baseurl
|
|
||||||
try:
|
try:
|
||||||
assert (isinstance(self.baseurl, str))
|
if not (self.webserver[-1] == "/"):
|
||||||
if not (self.baseurl is None) and not (self.baseurl[-1] == "/"):
|
self.webserver = self.webserver + "/"
|
||||||
self.baseurl = self.baseurl + "/"
|
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class APplusServerConnection:
|
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
|
:param settings: die Einstellungen für die Verbindung mit dem APplus Server
|
||||||
:type settings: APplusAppServerSettings
|
:type settings: APplusAppServerSettings
|
||||||
"""
|
"""
|
||||||
def __init__(self, settings: APplusAppServerSettings) -> None:
|
def __init__(self, settings: APplusServerSettings) -> None:
|
||||||
userEnv = settings.user
|
userEnv = settings.user
|
||||||
if (settings.env):
|
if (settings.env):
|
||||||
userEnv += "|" + settings.env
|
userEnv += "|" + settings.env
|
||||||
|
|
||||||
session = Session()
|
sessionApp = Session()
|
||||||
session.auth = HTTPBasicAuth(userEnv, "")
|
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.clientCache: Dict[str, Client] = {}
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
self.appserverUrl = "http://" + settings.appserver + ":" + str(settings.appserverPort) + "/"
|
self.appserverUrl = "http://" + settings.appserver + ":" + str(settings.appserverPort) + "/"
|
||||||
|
|
||||||
def getClient(self, package: str, name: str) -> Client:
|
def getAppClient(self, package: str, name: str) -> Client:
|
||||||
"""Erzeugt einen zeep - Client.
|
"""Erzeugt einen zeep - Client für den APP-Server.
|
||||||
Mittels dieses Clients kann die WSDL Schnittstelle angesprochen werden.
|
Mittels dieses Clients kann die WSDL Schnittstelle angesprochen werden.
|
||||||
Wird als *package* "p2core" und als *name* "Table" verwendet und der
|
Wird als *package* "p2core" und als *name* "Table" verwendet und der
|
||||||
resultierende client "client" genannt, dann kann
|
resultierende client "client" genannt, dann kann
|
||||||
@ -76,11 +86,34 @@ class APplusServerConnection:
|
|||||||
:return: den Client
|
:return: den Client
|
||||||
:rtype: Client
|
:rtype: Client
|
||||||
"""
|
"""
|
||||||
url = package+"/"+name
|
cacheKey = "APP:"+package+"/"+name
|
||||||
try:
|
try:
|
||||||
return self.clientCache[url]
|
return self.clientCache[cacheKey]
|
||||||
except:
|
except:
|
||||||
fullClientUrl = self.appserverUrl + url + ".jws?wsdl"
|
fullClientUrl = self.appserverUrl + package+"/"+name + ".jws?wsdl"
|
||||||
client = Client(fullClientUrl, transport=self.transport)
|
client = Client(fullClientUrl, transport=self.transportApp)
|
||||||
self.clientCache[url] = client
|
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".
|
||||||
|
|
||||||
|
: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
|
return client
|
||||||
|
@ -22,7 +22,7 @@ class APplusSysConf:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, server: 'APplusServer') -> None:
|
def __init__(self, server: 'APplusServer') -> None:
|
||||||
self.client = server.getClient("p2system", "SysConf")
|
self.client = server.getAppClient("p2system", "SysConf")
|
||||||
self.cache: Dict[str, type] = {}
|
self.cache: Dict[str, type] = {}
|
||||||
|
|
||||||
def clearCache(self) -> None:
|
def clearCache(self) -> None:
|
||||||
|
@ -392,7 +392,7 @@ class SqlConditionBinComp(SqlConditionPrepared):
|
|||||||
:type value2: SqlValue
|
:type value2: SqlValue
|
||||||
"""
|
"""
|
||||||
def __init__(self, op: str, value1: SqlValue, 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")
|
raise Exception("SqlConditionBinComp: value not provided")
|
||||||
|
|
||||||
cond = "({} {} {})".format(formatSqlValue(value1), op, formatSqlValue(value2))
|
cond = "({} {} {})".format(formatSqlValue(value1), op, formatSqlValue(value2))
|
||||||
|
Reference in New Issue
Block a user