getWebClient implementiert

This commit is contained in:
Thomas Türk 2023-07-26 16:00:22 +02:00
parent b05b5de039
commit 599339a270
12 changed files with 148 additions and 75 deletions

View File

@ -1,5 +1,11 @@
# Changelog # Changelog
## 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

View File

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

View File

@ -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`).

View File

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

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.0.1' version = '1.1.0'
release = '1.0.1' release = '1.1.0'
# -- 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

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

View File

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

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "PyAPplus64" name = "PyAPplus64"
version = "1.0.1" version = "1.1.0"
authors = [ authors = [
{ name="Thomas Tuerk", email="kontakt@thomas-tuerk.de" }, { name="Thomas Tuerk", email="kontakt@thomas-tuerk.de" },
] ]

View File

@ -35,14 +35,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()
""" """
@ -60,9 +59,9 @@ 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.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")
def reconnectDB(self) -> None: def reconnectDB(self) -> None:
try: try:
@ -130,9 +129,9 @@ class APplusServer:
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 +144,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]:
""" """
@ -220,10 +232,10 @@ class APplusServer:
return self.client_nummer.service.nextNumber(obj) return self.client_nummer.service.nextNumber(obj)
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 +263,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',

View File

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

View File

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

View File

@ -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: