diff --git a/Changelog.md b/Changelog.md index 3b4cd9f..334c9b2 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,11 @@ # 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 - Code-Cleanup mit Hilfe von flake8 - Bugfix: neue Python 3.10 Syntax entfernt diff --git a/README.md b/README.md index 970dca0..8b98ba4 100644 --- a/README.md +++ b/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/) diff --git a/docs/source/abhaengigkeiten.rst b/docs/source/abhaengigkeiten.rst index dbcce64..9db01bb 100644 --- a/docs/source/abhaengigkeiten.rst +++ b/docs/source/abhaengigkeiten.rst @@ -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,6 @@ 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`). \ No newline at end of file +Sollen Excel-Dateien mit Pandas erzeugt, werden, so muss Pandas, SqlAlchemy und xlsxwriter installiert sein +(`python -m pip install pandas sqlalchemy xlsxwriter`). + diff --git a/docs/source/anwendungen.rst b/docs/source/anwendungen.rst index 9f80cac..747f779 100644 --- a/docs/source/anwendungen.rst +++ b/docs/source/anwendungen.rst @@ -85,10 +85,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.server_conn.getAppClient("p2system", "SysConf"); print (client.service.getString("STAMM", "MYLAND")) diff --git a/docs/source/conf.py b/docs/source/conf.py index cdb89fb..0154e20 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -14,8 +14,8 @@ sys.path.append('../src/') project = 'PyAPplus64' copyright = '2023, Thomas Tuerk' author = 'Thomas Tuerk' -version = '1.0.1' -release = '1.0.1' +version = '1.1.0' +release = '1.1.0' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/examples/applus-server.yaml b/examples/applus-server.yaml index a5ed87e..16970c1 100644 --- a/examples/applus-server.yaml +++ b/examples/applus-server.yaml @@ -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", diff --git a/examples/read_settings.py b/examples/read_settings.py index a00b9f9..bbce997 100644 --- a/examples/read_settings.py +++ b/examples/read_settings.py @@ -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("masterdata/artikel.asmx") + print("ARTIKEL-ASMX Date:", client.service.getServerDate()) if __name__ == "__main__": main(applus_configs.serverConfYamlTest) diff --git a/pyproject.toml b/pyproject.toml index 4955c37..b157409 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "PyAPplus64" -version = "1.0.1" +version = "1.1.0" authors = [ { name="Thomas Tuerk", email="kontakt@thomas-tuerk.de" }, ] diff --git a/src/PyAPplus64/applus.py b/src/PyAPplus64/applus.py index 40e363f..a07bf4f 100644 --- a/src/PyAPplus64/applus.py +++ b/src/PyAPplus64/applus.py @@ -35,14 +35,13 @@ 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() """ @@ -60,9 +59,9 @@ class APplusServer: self.scripttool: applus_scripttool.APplusScriptTool = applus_scripttool.APplusScriptTool(self) """erlaubt den einfachen Zugriff auf Funktionen des ScriptTools""" - self.client_table = self.server_conn.getClient("p2core", "Table") - self.client_xml = self.server_conn.getClient("p2core", "XML") - self.client_nummer = self.server_conn.getClient("p2system", "Nummer") + self.client_table = self.server_conn.getAppClient("p2core", "Table") + self.client_xml = self.server_conn.getAppClient("p2core", "XML") + self.client_nummer = self.server_conn.getAppClient("p2system", "Nummer") def reconnectDB(self) -> None: try: @@ -130,9 +129,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 +144,20 @@ 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". + + :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]: """ @@ -220,10 +232,10 @@ class APplusServer: return self.client_nummer.service.nextNumber(obj) 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): @@ -251,20 +263,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', diff --git a/src/PyAPplus64/applus_scripttool.py b/src/PyAPplus64/applus_scripttool.py index 7a5cb07..3b3b2c6 100644 --- a/src/PyAPplus64/applus_scripttool.py +++ b/src/PyAPplus64/applus_scripttool.py @@ -57,7 +57,7 @@ class APplusScriptTool: """ def __init__(self, server: APplusServer) -> None: - self.client = server.getClient("p2script", "ScriptTool") + self.client = server.getAppClient("p2script", "ScriptTool") def getCurrentDate(self) -> str: return self.client.service.getCurrentDate() diff --git a/src/PyAPplus64/applus_server.py b/src/PyAPplus64/applus_server.py index 5ba0031..db0ce01 100644 --- a/src/PyAPplus64/applus_server.py +++ b/src/PyAPplus64/applus_server.py @@ -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 + 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,34 @@ 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". + + :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 diff --git a/src/PyAPplus64/applus_sysconf.py b/src/PyAPplus64/applus_sysconf.py index 312a93a..adb154c 100644 --- a/src/PyAPplus64/applus_sysconf.py +++ b/src/PyAPplus64/applus_sysconf.py @@ -22,7 +22,7 @@ class APplusSysConf: """ def __init__(self, server: 'APplusServer') -> None: - self.client = server.getClient("p2system", "SysConf") + self.client = server.getAppClient("p2system", "SysConf") self.cache: Dict[str, type] = {} def clearCache(self) -> None: