Request von API behandeln

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Benutzeravatar
Prinz_Monty
User
Beiträge: 18
Registriert: Mittwoch 24. Oktober 2018, 09:43

Hallo,

vorweg ich bin blutiger Anfänger und freue mich über konstruktive Kritik!

Folgendes ich versuche auf die REST API von ISPConfig zu zugreifen dort möchte ich mir Diverse Domains ausgeben lassen.
Bekomme das auch soweit hin. Ich glaube aber nicht das ich es richtig mache. Gerade auch wie ich die Session behandel.

Dazu kommt noch das ich eigentlich alle Domains ausgeben möchte nicht nur eine Spezielle. Vielleicht hat ja jemand einen Tip
für mich. Und wie gesagt verzeiht ich Arbeite mich da gerade erst mühsam ein.

Hier mal mein Code:

Code: Alles auswählen

import json
import requests

urlLogin = "https://localhost:8080/remote/json.php?login"
payload = {'username': 'USER_NAME', 'password': 'USER_PW'}
headers = {'content-type': 'application/json'}

sid = requests.post(urlLogin, data=json.dumps(payload), headers=headers).json()

try:
    if(sid['code'] == 'ok'):
        i = sid['response']
        urlJson = "https://localhost/remote/json.php?sites_web_domain_get"

        params = {
            'session_id': i, # Hier die Session ID die ich oben schon bekommen habe (Das kann doch so nicht korrekt sein?)
            'primary_id': '46'  # Hier übermittel ich die ID der Domain die ich mir ausgeben lassen möchte
        }
        domain = requests.get(urlJson, data=json.dumps(params)).json()

        print(domain["response"]["domain"] +
              domain["response"]["traffic_quota"] +
              domain["response"]["ssl_letsencrypt"])
    else:
        print('Passwort falsch?')
except requests.exceptions.HTTPError as err:
    print(err)
Wie gesagt mir reichen schon Tips muss nicht gleich ganzes Refactoring sein
Mögen hätt ich schon wollen, aber dürfen habe ich mich nicht getraut. *Karl Valentin
Benutzeravatar
__blackjack__
User
Beiträge: 14030
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Prinz_Monty: Namen werden in Python klein_mit_unterstrichen geschrieben. Ausnahmen Konstanten (KOMPLETT_GROSS) und Klassen (MixedCase). Also eher `login_url` statt `urlLogin` (was irgendwie auch falsch herum ist).

Ich finde es ja ein bisschen komisch das bei der einen URL der Port 8080 angegeben wird, und bei der anderen nicht. Sind das tatsächlich zwei verschiedene Ziele? *Dann* würde mich in der Tat auch wundern das man bei der zweiten URL die Session-ID angeben kann die man bei der ersten URL bekommen hat.

JSON kann man mit `requests` einfacher senden: statt über `data`, selbst als JSON kodieren, und manuell den 'Content-Type'-Header zu setzen, kann man einfach das `json`-Argument verwenden.

Die Namen `sid` und `domain` sind falsch weil beides Antworten sind die jeweils die Session-ID und Daten zu einer Domain *enthalten* aber noch nicht selbst diese Werte sind. Das `i` müsste `sid` heissen.

Die Ausnahmebehandlung macht so keinen Sinn. Zum einen umfasst der ``try``-Block gar nicht alles was diese Ausnahme auslösen kann – die kann ja auch beim Anmelden passieren. Zum anderen ist es wenig sinnvoll einfach nur die Ausnahme selbst als Text auszugeben und effektiv den Traceback zu unterdrücken. Wenn ein Fehler auftritt den man nicht sinnvoll behandeln kann, möchte man doch so viel wie möglich Informationen um auf die Ursache schliessen zu können.

``if`` ist keine Funktion, also sollte man da mindestens ein Leerzeichen nach setzen. Die Klammern sind aber insgesamt überflüssig.

Die Ausgabe im ``else`` sieht so geraten aus. Klar kann das Passwort falsch sein. Aber auch der Login. Oder es wahren nicht alle Daten in der Anfrage die erwartet wurden. Oder die Daten hatten irgendwelche anderen Probleme (illegale Zeichen im Benutzernamen, zu lang, zu kurz, Vollmond, …). Ich habe auf die schnelle keine Dokumentation der API gefunden, nur ein paar Beispiele im Netz. Die haben suggeriert, das es neben 'code' und 'response' auch 'message' in den Antworten gibt, wo im Fehlerfall wohl etwas drinsteht.

Wieso denkst Du das übergeben der Session-ID, die Du beim Anmelden bekommen hast, müsste man nicht im folgenden an alle anderen Aufrufe übergeben? Woher soll der Server denn sonst wissen wer Du bist und das Du das darfst, wenn Du ihm das nicht jedes mal durch vorzeigen der Session-ID beweisen kannst? Sonst könnte da ja jeder kommen und Anfragen stellen ohne sich angemeldet zu haben.

Wenn ich die Beispiele im Netz richtig verstanden habe, kann man statt einer konkreten 'primary_id' auch ein JSON-Objekt mit Filterkriterien angeben, womit man dann zum Beispiel alle aktiven Domains abfragen kann. Das sollte aber auch in der API-Dokumentation stehen.

Bei der zweiten API-Anfrage prüfst Du den 'code' der Antwort gar nicht‽

Die Verkettung mit ``+`` bei der Ausgabe der Domain-Werte ist „unpythonisch“ – es gibt Zeichenkettenformatierung für so etwas, und auch irgendwie falsch, denn da werden die Werte ja völlig ohne Leerzeichen aneinander geklebt.

Ich komme dann letztendlich ungefähr hier raus (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
import requests

BASE_URL = 'https://localhost:8080/remote/json.php'
LOGIN_URL = BASE_URL + '?login'
SITES_WEB_DOMAIN_GET_URL = BASE_URL + '?sites_web_domain_get'


def main():
    auth_data = {'username': 'USER_NAME', 'password': 'USER_PW'}
    response = requests.post(LOGIN_URL, json=auth_data).json()
    if response['code'] == 'ok':
        session_id = response['response']
        arguments = {'session_id': session_id, 'primary_id': {'active': True}}
        response = requests.get(SITES_WEB_DOMAIN_GET_URL, json=arguments).json()
        if response['code'] == 'ok':
            for domain in response['response']:
                print(
                    f'{domain["domain"]} {domain["traffic_quota"]}'
                    f' {domain["ssl_letsencrypt"]}'
                )
        else:
            print(
                f'Fehler beim Abfragen der aktiven Domains:'
                f' {response["message"]}'
            )
    else:
        print(f'Fehler beim Anmelden: {response["message"]}')


if __name__ == '__main__':
    main()
Wobei ich wahrscheinlich noch das externe Modul `addict` verwenden und die `json()`-Ergebnisse in dessen `Dict` verpacken würde um etwas ”schöner” auf die Ergebnisse zugreifen zu können.

Und spätestens bei einer dritten Wiederholung des immer gleichen API-Aufrufrituals mit dem 'code'/'ok' würde ich das mindestens in einer Funktion isolieren, wenn nicht gar in einer Klasse die dann auch gleich die Session-ID kapselt.
„A life is like a garden. Perfect moments can be had, but not preserved, except in memory. LLAP” — Leonard Nimoy's last tweet.
Benutzeravatar
snafu
User
Beiträge: 6862
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

@__blackjack__: Huh, f-Strings in deinem Code...? Ich habe dich immer als Verfechter von Syntax wahrgenommen, die (etwas überspitzt) auch noch unter Python 2.5 laufen könnte. Sind das die neuen Unterstriche, die deine Einstellung zum Modernen gewendet haben? ;-)
Benutzeravatar
__blackjack__
User
Beiträge: 14030
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@snafu: Ich verfechte ”alte” Syntax immer bis ich mal alle gefühlt 100 Jahre meine LTS-Systeme aktualisiere. Wobei die f-Strings in der Tat grenzwertig sind, denn Python 3.5 ist bei aktuellen Debian-Systemen normal, also kann man die da noch nicht verwenden.

Ansonsten habe ich halt doch langsam angefangen den Umstieg/die Portierung auf Python 3 für meine beruflichen Sachen zu planen. Also zumindest bei den Projekten die aktiv in der Entwicklung sind, und immer wenn ich administrative Skripte sowieso wegen irgend etwas anderem mal wieder anfassen muss, schaue ich das dabei mindestens ein Schritt für die Portierung gemacht wird.
„A life is like a garden. Perfect moments can be had, but not preserved, except in memory. LLAP” — Leonard Nimoy's last tweet.
Benutzeravatar
Prinz_Monty
User
Beiträge: 18
Registriert: Mittwoch 24. Oktober 2018, 09:43

Danke für die ausführliche Analyse! Lern gerade viel dazu. Ich gehe jetzt nicht auf alle Tips & Korrekturen ein aber habe alles zur Kenntnis genommen.

Ich hatte die URLs bearbeitet, daher fehlte bei dem einen der Port 8080. Der gehört dort natürlich auch hin.

Das an einigen stellen falsche Syntax verwendet wurde liegt daran das ich bisher nur in PHP Programmiert habe.

Genau, in 'Message' steht im Fehlerfall etwas drin. Es gibt auch keine Dokumentation zur API. Macht das ganze nicht gerade einfacher.

Ich hatte mit der Session so meine Probleme aber so wie du mir den Code formatiert hast verstehe ich es besser.

Er hat auch sofort funktioniert musste nur {'active': True} auf {'active': 'y'} ändern

Das mit den f strings fand ich auch interessant hab ich mir gleich mal in der Dokumentation angeschaut.

Ich hab mir deine Empfehlung mal zu Herzen genommen und das Session Handling in eine Klasse gepackt. Vermutlich hab ich da wieder alles falsch gemacht
aber sieh selbst. :-)

Code: Alles auswählen

import requests

BASE_URL = 'https://localhost:8080/remote/json.php'
LOGIN_URL = BASE_URL + '?login'


class LoginAPICall:
    def __init__(self, BASE_URL, LOGIN_URL):
        self.BASE_URL = BASE_URL
        self.LOGIN_URL = LOGIN_URL

        self.auth_data = {'username': 'USERNAME', 'password': 'PASSWORD'}
        self.response = requests.post(LOGIN_URL, json=self.auth_data).json()

    def connect(self):
        if self.response['code'] == 'ok':
            self.session_id = self.response['response']
            print(f'Erfolgreich angemeldet: {self.session_id}')

            return self.session_id
        else:
            print(f'Fehler beim Anmelden: {self.response["message"]}')

sessionID = LoginAPICall(BASE_URL, LOGIN_URL)
sessionID.connect()            
       
Response: 'Erfolgreich angemeldet: .....SESSIONID.....'
Mögen hätt ich schon wollen, aber dürfen habe ich mich nicht getraut. *Karl Valentin
Benutzeravatar
snafu
User
Beiträge: 6862
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Wenn ich dann auch mal nörgeln darf: print()-Aufrufe gehören selten direkt in Klassen. Die Ausgabe beim ersten print() würde man wohl eher an einen Logger leiten und das zweite print() ersetzt man besser durch eine Exception.
Benutzeravatar
Prinz_Monty
User
Beiträge: 18
Registriert: Mittwoch 24. Oktober 2018, 09:43

Du darfst :)
Mögen hätt ich schon wollen, aber dürfen habe ich mich nicht getraut. *Karl Valentin
Benutzeravatar
Prinz_Monty
User
Beiträge: 18
Registriert: Mittwoch 24. Oktober 2018, 09:43

@sanfu:

Über welche library macht man den am besten das logging über "loggin" ?

https://docs.python.org/3/library/logging.html
Mögen hätt ich schon wollen, aber dürfen habe ich mich nicht getraut. *Karl Valentin
Benutzeravatar
snafu
User
Beiträge: 6862
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Jepp, genau die hab ich gemeint. Hier würde sich logging.info() anbieten.
Sirius3
User
Beiträge: 18265
Registriert: Sonntag 21. Oktober 2012, 17:20

@Prinz_Monty: self.BASE_URL und self.LOGIN_URL werden gar nicht benutzt, und wenn man sie benutzen würde, wären sie (ebenso wie die Argumente von __init__) komplett klein geschrieben, denn sobald man sie als Argumente übergibt, sind es keine Konstanten mehr. Was aber Argumente sind, ist Username und Passwort.
Dass in __init__ die ganze Arbeit gemacht wird, in `connect` dann nur noch geprüft wird, ob die Arbeit erfolgreich war, ist überraschend.
Der Name der Klasse deutet schon an, dass es eigentlich keine Klasse ist. Nur für einen Login-Call braucht man keine Klasse, das wäre eine Funktion. Wenn Du aber noch die weiteren API-Calls mit in die Klasse aufnimmst, sieht das anders aus, dann müßte die Klasse aber ISPConfig heißen.
Ebenso ist `sessionID` für die Instanz dieser Klasse falsch. Das Objekt enthält zwar eine sessionID, ist aber keine.

Das ganze könnte so aussehen:

Code: Alles auswählen

import requests
import logging
logger = logging.getLogger("ispconfig")

BASE_URL = 'https://localhost:8080/remote/json.php'

class ISPConfig:
    def __init__(self, base_url):
        self.base_url = base_url
        self.session_id = None

    def _call(self, command, **params):
        if self.session_id:
            params['session_id'] = session_id
        response = requests.post(self.base_url, params=command, json=params).json()
        if response['code'] != 'ok':
            raise RuntimeError(f'Fehler bei {command}: {response["message"]}')
        return response['response']

    def login(self, username, password):
        self.session_id = None
        self.session_id = self._call('login', username=username, password=password)
        logger.info('Erfolgreich angemeldet: %s', self.session_id)

    def sites_web_domain_get(self, primary_id):
        return self._call('sites_web_domain_get', primary_id=primary_id)

isp_config = ISPConfig(BASE_URL)
isp_config.login("USER", "PASSWD")
print(isp_config.sites_web_domain_get("12"))
Benutzeravatar
Prinz_Monty
User
Beiträge: 18
Registriert: Mittwoch 24. Oktober 2018, 09:43

Sirius3: Ich danke dir auch für die Analyse. Ich lerne gerade sehr viel dazu. Ich Arbeite gerade einige Tutorials durch um es besser zu verstehen. Aber es hilft sehr funktionierende beispiele zu haben mit denen ich bisschen rum spielen kann. Ich hab mir jetzt über Docker noch einen Testserver für ISPConfig installiert damit ich nicht mit echten Daten rum werkel. Wenn ich mal was finales fertig hab was ich zeigen kann stell ich es hier rein.

Deine Klasse hat sofort funktioniert und ich konnte Sie ohne Probleme erweitern. Musste nur

Code: Alles auswählen

params['session_id'] = session_id
in

Code: Alles auswählen

params['session_id'] = self.session_id
ändern.

Hiermit kann man sich alle API Funktionen auflisten:

Code: Alles auswählen

class ISPConfig:
	...
	
    	def get_function_list(self, function_list):
        return self._call('get_function_list', primary_id=function_list)

print(isp_config.get_function_list("1"))
Und hier noch ein beispiel wie man Daten ändern kann:

Code: Alles auswählen

class ISPConfig:
	...
	
	# Add/Update notice from clients
	def client_update(self, client_id, affected_row):
        return self._call('client_update', client_id=client_id, params={'notes': affected_row})
        
isp_config.client_update('1', "This is a notice.")
Mögen hätt ich schon wollen, aber dürfen habe ich mich nicht getraut. *Karl Valentin
Antworten