2 Skripte kombinieren (sched + discord.py)

  • Hallo zusammen,

    mein Ziel ist es einen eigenen Discord Erinnerungsbot zu erstellen, der aus dem jeweiligen Eintrag aus der data.toml wöchentlich eine Erinnerung in Discord schickt. Die Uhrzeit und Wochentag lässt sich aus date entnehmen, wann das Ereignis stattfindet. Anschließend wird noch eine Vorlaufzeit (remind_offset) abgezogen, sodass die Erinnerung um x Minuten vor Stattfinden des Ereignisses abgeschickt wird. Um die Ereignisse innerhalb von Python zu erstellen und planen, verwende ich die Modul sched. Dieser Part funktioniert auch nach meinen Vorstellungen, solange die Ausgabe mit einem print() erfolgt und nicht die wirkliche Erinnerung über Discord gesendet wird. Ebenfalls funktioniert ein alleinstehendes Discord Skript, mit dem ich nach Start des Skriptes eine Testnachricht an den gewünschten Discord Server und Kanal sende. Mein Problem tritt auf sobald ich beides vereinen möchte, vermutlich auf Grund meiner mangelnden Kenntnisse von async. Bzw. diese beiden Befehle vertragen sich nicht: client.run(toml.load("config.toml")["dc_token"]) und scheduler.run(blocking=True). Ändere ich das blocking in False funktioniert es auch nicht mehr. Bei einer Kombination habe ich es zwar geschafft beides zu vereinen aber dann bekomme ich Fehler von async wie diese hier

    Zugehöriger Kombinationsversuch:

    Auch habe ich schon damit begonnen blind await und async bei diversen Funktionen einzubauen, aber leider noch kein Erfolg.

    Im Anschluss poste ich die getrennten Varianten, die soweit funktionieren, inkl der data.toml.


    Im Grunde muss ich es schaffen, dass die Funktion ab Zeile 12 in gebietserinnerung.py def send_reminder(scheduler, remind_offset, territory, text, target_ts, _): statt der print Ausgabe den gewünschten Inhalt in Discord sendet. Aktuell weiß ich aber nicht mehr weiter wie ich das kombinieren kann und hoffe auf eure Hilfe.

  • Hallo,

    mir ist jetzt tatsächlich eine funktionierende Lösung eingefallen, weshalb ich meinen Eingangspost nicht editieren wollte, sondern die Lösung in einem separaten Post herzeigen möchte. Die Lösung ist zwar nicht elegant, aber sie funktioniert. Mittels subprocess starte ich beim Eintreten des Ereignisses das Discord Skript und übergebe dabei als Parameter den zu postenden Text, anschließend kann sich der Discord Bot sofort wieder beenden.

    Skript welches Dauerhaft läuft:

    und hier das Skript, welches durch subproccess aufgerufen wird

    und zur Vollständigkeit noch utils und data.toml

    Obwohl ich nun eine funktionierende Lösung habe, würde es mich dennoch interessieren, wie und ob es anders auch zu lösen ist.

  • Hi,


    kurze Zwischenfrage, ich habe mal die Fehlermeldung gegooglt und bin zwei mal auf diese Lösung gestoßen:

    "Asyncio Event Loop is Closed" when getting loop
    When trying to run the asyncio hello world code example given in the docs: import asyncio async def hello_world(): print("Hello World!") loop =…
    stackoverflow.com


    Hast du das schon probiert?


    Grüße

    Dennis

    🎧 I'm strapped into my bed,
    I've got electrodes in my head.
    My nerves are really bad,
    it's the best time I've ever had. 🎧

  • Nein, mir wäre jetzt aber auch die Stelle nicht bekannt, an der ich die loop bewusst schließe, sodass ich an der Stelle die loop wieder neu starten müsste. An welcher Stelle würdest du die loop neu starten? Genau in der Zeile bzw. eine zuvor wo der Fehler auftritt? Oder meinst du gar nicht das neustarten sondern viel mehr diesen Teil hier?

    Code
    import platform
    if platform.system()=='Windows':
        asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())

    Zu Problemen wird es bestimmt deswegen kommen, da discord.py mit asyncroner Programmierung arbeitet (wo ich mir immer sehr schwer tue) und sched Multi Thread tauglich ist. Aber reine Vermutung....

  • Ja ich meinte eher den Teil, da du Windows nutzt, und ein User auf SO geschrieben hat, dass es da seit einer Änderung Probleme gibt, hätte ich es vermutlich mal so versucht:


    Grüße

    Dennis

    🎧 I'm strapped into my bed,
    I've got electrodes in my head.
    My nerves are really bad,
    it's the best time I've ever had. 🎧

  • Teilerfolg, das Skript stürzt nicht ab, gibt jedoch dennoch gleich nach Start einer Meldung aus. Eine Nachricht an Discord kommt aber noch nicht an. Dann werde ich mal versuchen diese awaited Meldungen noch zu beheben.

    Code
    C:\Users\***\Nextcloud\P_Python\tabellenanzeige\test.py:57: RuntimeWarning: coroutine 'create_territory_capture' was never awaited
      create_territory_captures_after_start(scheduler,
    RuntimeWarning: Enable tracemalloc to get the object allocation traceback
  • Aus dem Bauch raus (habe das noch nie wirklich gebraucht):

    Code
    @client.event
    async def on_ready():
        scheduler = sched.scheduler(time.time, time.sleep)
        await create_territory_captures_after_start(scheduler,
                                              REMIND_OFFSET,
                                              toml.load("data.toml")["territories"])
        scheduler.run(blocking=True)

    🎧 I'm strapped into my bed,
    I've got electrodes in my head.
    My nerves are really bad,
    it's the best time I've ever had. 🎧

  • Beim schauen, was der Code so macht, sind mir zwei, drei Sachen aufgefallen, falls es dich interessiert:

    `search_work_channel' könnte man kompakter schreiben. Ich würde `channels` nicht extra als Name vergeben, sondern direkt über `client.get_all_channels()` iterieren. `work_channel` würde ich auch nicht mit `None` vorbelgen, weil `client.get_channel` gibt `None` zurück wenn die ID nicht gefunden wid. Aber eigentlich ist das auch egal, ich würde die `if`-Abfrage bei Erfolg mit return verlassen und ansonsten gibt die Funktion automatisch `None` zurück.

    In `send_reminder` wird es zu einem `AttributError` kommen, falls `workchannel` tatsächlich mal `None` sein sollte.

    Ich weis du magst list-comprehensions nicht sonderlich, ich finde es in `create_territory_captures_after_start` lesbarer.

    Jetzt müsste man noch was dagegen tun, das `client` global verfügbar ist und dafür `data.toml` und `config.toml` als Konstanten definieren.

    Würde dann so aussehen (für `client` fällt mir gerade nocht nichts ein):


    Grüße

    Dennis

    🎧 I'm strapped into my bed,
    I've got electrodes in my head.
    My nerves are really bad,
    it's the best time I've ever had. 🎧

  • In on_ready() wird Code aufgerufen der unendlich lange läuft aber nie ein await ausführt. Das darf man in async Funktionen nicht machen weil dann alles blockiert. Nicht-blockierend geht auch nicht so einfach weil das bei `sched` dann über einen Thread gelöst ist, und man darf nicht einfach so async Funktionen aus anderen Threads heraus aufrufen. Im Grunde das gleich Thema wie bei Rückruffunktionen in GUI-Rahmenwerken.

    Lesestoff aus der asyncio-Dokumentation: Concurrency and Multithreading.

    “The object-oriented version of 'Spaghetti code' is, of course, 'Lasagna code'. (Too many layers)“ — Roberto Waltman

  • Der Lesestoff beweist mir einmal mehr, dass ich viel zu wenig asyncio verstehe.

    Durch deine Antwort __blackjack__ fühle ich mich bestätigt, dass asyncio (discord.py) und sched einfach wirklich nicht gut zusammen passen und ich mit meiner gezeigten Lösung aus #2 mit subprocess gut fahre. Somit werde ich mal beginnen dem Code aus #2 den Feinschliff zu verpassen.

    Vielen Dank für eure Hilfe :thumbup:

  • Der Lesestoff beweist mir einmal mehr, dass ich viel zu wenig asyncio verstehe.

    Zu deiner Ehrenrettung: Das Probleme hatte / habe ich auch latent. Also ich verstehe schon vom Prinzip, wie asyncio funktioniert - aber ich habe auch keine Anwendung dafür und damit keine Praxiserfahrung. Bei asyncio kommt IMHO noch dazu, dass man das auch nicht "mal einfach so" ausprobiert bzw. die "Hallo Welt" asyncio Bespiele mega-trivial sind und damit eine ziemlich große Lücke zu realen Anwendungen kommt.

    Wobei halt zusätzliche Threads im asyncio Loop schon ein bisschen schwierig vorzustellen ist - das ist ja Nebenläufigkeit in der Nebenläufigkeit.

    Gruß, noisefloor

  • Hofei Naja, `subprocess` um aus Python Python zu starten für etwas was eigentlich *ein* Programm ist, ist keine gute Lösung. `asyncio` und Threads passen insoweit zusammen, das der Link ja beschreibt wie man von anderen Threads aus async-Coroutinen anstossen kann. Also genau das was hier gesucht ist.

    `get_remaining_time()` wird nirgends aufgerufen. Da ist auch Code drin den es noch mal in `get_target_ts()` gibt. Wenn man die Funktion also nicht entfernt, sollte man den gemeinsamen Code in eine weitere Funktion auslagern.

    `timestamp()` und `fromtimestamp()` wird zu oft benutzt. Wenn ich das richtig sehe sollte man da nicht immer sinnlos hin und her wandeln, denn eigentlich braucht man nur *einen* `timestamp()` aufruf und gar keinen `fromtimestamp()`, denn nur an einer einzigen Stelle wird tatsächlich der klassische Unix-Timestamp benötigt. Und dann wird die `get_remind_ts()` so trivial — eine einfache Subtraktion der beiden Argumente, dass man die Funktion weg lassen kann.

    Thema Umwandlungen: Man sollte möglichst schnell, wenn Daten das Programm betreten in einen passenden Datentyp umwandeln. Also Beispielsweise `remind_offset` gleich nach dem man es aus der Konfiguration holt in ein `timedelta`-Objekt umwandeln, statt die nackte Zahl herum zu reichen und dann an mehr als einer Stelle in ein `timedelta`-Objekt umzuwandeln.

    `get_target_ts()` ist ein komischer Zwischenschritt. Statt das aufzurufen was dann `get_target_datetime()` mit einem Argument mehr aufruft, würde man eher eine Funktion schreiben die dieses Argument erstellt. Das wäre ja auch genau die Funktion die man herausziehen würde wenn `get_remaining_time()` im Quelltext bleiben würde.

    `get_target_datetime()` ist verwirrend. Da wird die lokale Zeit genommen, als UTC erklärt, was ja falsch ist, und Zeitanteile ersetzt und dann wird dieser falsche Wert wo das Datum lokal ist, aber als UTC deklariert wird in die mitteleuropäische Zeitzone umgewandelt. Sollte das nicht zufällig auch die lokale Zeitzone sein, wird es noch wirrer, weil dann *drei* Zeitzonen an diesem Spielchen beteiligt sind. Da sollte also mindestens mal `utcnow()` statt `now()` verwendet werden, damit man das auf maximal zwei beteiligte Zeitzonen begrenzt. Und man muss ich entscheiden an welcher Stelle man die Uhrzeit für das Gebiet setzt. Da das ansonsten mit der mitteleuropäischen verglichen wird, würde ich mal vermuten die Daten in der Konfigurationsdatei sollen auch für Mitteleuropa gelten und nicht für UTC‽

    Die ``while``-Schleife in der Funktion ist ein bisschen umständlich formuliert. Das ist eigentlich eine kopfgesteuerte Schleife bei der man wunderbar eine Abbruchbedingung formulieren kann und deren Körper dann aus einer einzigen Zeile besteht.

    Warum ``check=False`` bei `subprocess.run()`?

    Bei `create_text()` steht ein Teil des Textes in den `strftime()`-Aufrufen statt in der Zeichenkette‽ `strftime()` ist auch unnötig lang. Man kann das in der Formatspezifikation direkt angeben, ohne den Methodenaufruf.

    Die beiden TOML-Dateien sollten nur einmal geladen werden.

    `get_role_id()` ist unsinnigerweise async. Da passiert doch gar nichts was das erfordern würde. Die Funktion sollte auch in *jedem* Fall explizit etwas zurück geben und nicht nur wenn die Rolle gefunden wurde. Beziehungsweise eine Ausnahme auslösen wenn die Rolle nicht gefunden wird.

    `search_work_channel()` ist gegenüber `get_role_id()` unnötig kompliziert.

    Statt das `Channel`-Objekt in eine Zeichenkette umzuwandeln sollte man genau wie bei `get_role_id()` auf den Namen für den Vergleich zugreifen. Und ich bin auch etwas verwirrt, was der `get_channel()`-Aufruf soll. Dem wird die ID von `channel` übergeben. Bekommt man da dann nicht genau das `Channel`-Objekt zurück was man bereits *hat*, oder was ist der Unterschied zwischen `channel` und dem Rückgabewert.

    Man sollte `sched` mit `asyncio` zusammenbringen können wenn man `run()` vom Scheduler nicht-blockierrend aufruft, das warten selbst übernimmt mit `asyncio.sleep()` und in der Rückruffunktion vom Scheduler dann einen `Task` für das Versenden der Nachricht in der laufenden asyncio Ereignisschleife erzeugt.

    Das fühlt sich so ein bisschen an als wenn man gegen `sched` programmiert. Die `asyncio`-Ereignisschleife hat eine `call_at()`-Methode — ist also eigentlich selbst so ein bisschen ein Scheduler. Da braucht es nur ein bisschen Code um den Zeitpunkt zu errechnen. Wobei ich mir sicher bin, dass das auch schon jemand gemacht hat und man da ein fertiges Modul für finden dürfte.

    Achtung: Der Code enthält eine Fehlerquelle: Das `on_ready()`-Ereignis kann mehr als einmal ausgelöst werden. Das muss man also absichern gegen weitere Aufrufe. An der Stelle ein recht harter Grund da über mindestens eine eigene Klasse nachzudenken. Auch um das globale `client` loszuwerden. Aber auch für den Scheduler bietet sich eine Klasse an, wenn man sich anschaut wie viele Argumente da überall durchgereicht werden. Und das müssten eigentlich noch mehr sein, denn weder `DATA` noch `CONFIG` haben etwas auf Modulebene zu suchen.

    Zwischenstand (ungetestet):

    “The object-oriented version of 'Spaghetti code' is, of course, 'Lasagna code'. (Too many layers)“ — Roberto Waltman

  • Hallo und vielen Dank für die Mühe mit den ausführlichen Verbesserungshinweisen und auch vorgelegten Code. Diesen habe ich gleich einmal gestartet, folgende Ausgabe bekomme ich bei dem Start, trotz des Tracebacks stürzt das Skript aber nicht ab. Allerdings funktioniert es auch nicht, wie es soll. Zeitpunkte, die am selben Tag aber in der Vergangenheit liegen, lösen eine Benachrichtigung in Discord aus und der gewünschte Testzeitpunkt, den ich eigentlich erwarten würde, löst keine Benachrichtigung aus. Das wird da dran liegen, dass du die Zeitmechanik und die Zeitzonen geändert hast.

    Das ich auf alle Absatzblöcke eingehe und versuche bei mir zu ändern, bzw. deine Vorlage anzupassen, dass sie meinen Wünschen entspricht, dafür fehlt mir heute die Zeit. Fußball UCL ist ja heute .... Damit werde ich dann morgen beginnen.

    Jedoch möchte ich noch gleich folgendes beantworten:

    Ja, die Zeitangaben in der Toml Datei sind UTC, da sie so auch aus der entnommenen Quelle vorlegen und dann kann ich das 1:1 übernehmen und muss das auch nicht halbjährlich ändern.

    `get_remaining_time()` wird nirgends aufgerufen. Da ist auch Code drin den es noch mal in `get_target_ts()` gibt. Wenn man die Funktion also nicht entfernt, sollte man den gemeinsamen Code in eine weitere Funktion auslagern.

    Das liegt daran, dass alles was ich in utils.py ausgelagert habe von einem weiteren unabhängigen Skript verwendet wird. Die Funktion wird also benötigt, dass ich jedoch den redundanten Codeteil auslagern kann ist ein richtiger und sinnvoller Tipp. Zudem erwähnten weiteren Skript habt ihr sogar auch schonmal Hilfestellungen gegeben, und zwar hier. Auch werden noch weitere, bisher noch nicht existierende Skripte folgen.


    Bei asyncio kommt IMHO noch dazu, dass man das auch nicht "mal einfach so" ausprobiert bzw. die "Hallo Welt" asyncio Bespiele mega-trivial sind und damit eine ziemlich große Lücke zu realen Anwendungen kommt.

    Ja genau das ist eines der Probleme, mit denen ich zu kämpfen habe

    Bei sowas seh ich den Sinn nicht und wenn ich sowas sehe:

    erkenne ich keinen Unterschied/Vorteil zu dem hier


    Zu deiner Ehrenrettung: Das Probleme hatte / habe ich auch latent

    "Witzigerweise" habe ich gestern mit Dennis89 per Privat Nachricht über dich und asyncio geschrieben, dass du wohl einer hier bist, der damit am besten umgehen kann und dass ich dich mal fragen will, ob du nicht eine kleine Erweiterung zu PyTuDe machen willst und ein deutsches Tutorial zu asyncio machen willst. Hatte aber auch im Zuge gleich erwähnt, dass ich erstmal das Kontrolllesen von PyTuDe abschließen möchte. Das wird auf alle Fälle wieder fortgesetzt, will nur erst den Discord Erinnerungsbot zuverlässig in Betrieb haben. :thumbup:

  • "Witzigerweise" habe ich gestern mit Dennis89 per Privat Nachricht über dich und asyncio geschrieben, dass du wohl einer hier bist, der damit am besten umgehen kann

    Danke für die Fehleinschätzung ;) Nee, ich habe mich zwar früh (Python 3.4. und 3.5) mit asyncio beschäftigt und glaube wie gesagt auch, dass ich das Konzept etc. verstanden habe - aber meine Praxiserfahrung an realen Programmen liegt bei Null. Wenn mal was machen müsste würde ich wahrscheinlich auch erst Mal schauen, ob das nicht mit Trio besser lösbare wäre, weil das IMHO die bessere Doku und schönere API hat.

    und ein deutsches Tutorial zu asyncio machen willst.

    Da habe ich nicht genug Wissen für. Außerdem schreibe ich seit einiger Zeit einem kompakten Schnelleinstieg / -überblick Tutorial für Django. Das kann ich besser als asyncio.

    Gruß, noisefloor

  • Hofei Die Ausnahme ist leicht behoben: das zweite Argument von `scheduler` ist `None` weil ich dachte das wird nicht mehr aufgerufen wenn man das mit dem Warten selbst erledigt. Da einfach wieder `time.sleep()` rein, dann ist die Ausnahme weg. In `run()` wird ein ``time.sleep(0)`` aufgerufen, als Hack um den Thread ”aufzugeben”, damit ein anderer Thread dran kommen kann.

    “The object-oriented version of 'Spaghetti code' is, of course, 'Lasagna code'. (Too many layers)“ — Roberto Waltman

  • Wie du schon sagtest, die Ausnahme ist behoben. Danke. Dafür überschlägt sich die Ausgabe jetzt mit soetwas:

    Code
    <Task pending name='Task-140' coro=<send_reminder() running at C:\Users\***\Nextcloud\P_Python\stfc_gebietsassistent\standalone_test.py:45> cb=[set.discard()]>, <Task pending name='Task-508' coro=<send_reminder() running at C:\Users\***\Nextcloud\P_Python\stfc_gebietsassistent\standalone_test.py:45> cb=[set.discard()]>, <Task pending name='Task-141' coro=<send_reminder() running at C:\Users\***\Nextcloud\P_Python\stfc_gebietsassistent\standalone_test.py:45> cb=[set.discard()]>, <Task pending name='Task-509' coro=<send_reminder() running at C:\Users\***\Nextcloud\P_Python\stfc_gebietsassistent\standalone_test.py:45> cb=[set.discard()]>, <Task pending name='Task-142' coro=<send_reminder() running at C:\Users\***\Nextcloud\P_Python\stfc_gebietsassistent\standalone_test.py:45> cb=[set.discard()]>, <Task pending name='Task-510' coro=<send_reminder() running at C:\Users\***\Nextcloud\P_Python\stfc_gebietsassistent\standalone_test.py:45> cb=[set.discard()]>, <Task pending name='Task-143' coro=<send_reminder() running at C:\Users\***\Nextcloud\P_Python\stfc_gebietsassistent\standalone_test.py:45> cb=[set.discard()]>, <Task pending name='Task-511' coro=<send_reminder() running at C:\Users\***\Nextcloud\P_Python\stfc_gebietsassistent\standalone_test.py:45> cb=[set.discard()]>, <Task pending name='Task-144' coro=<send_reminder() running at C:\Users\***\Nextcloud\P_Python\stfc_gebietsassistent\standalone_test.py:45> cb=[set.discard()]>, <Task pending name='Task-512' coro=<send_reminder() running at C:\Users\***\Nextcloud\P_Python\stfc_gebietsassistent\standalone_test.py:45> cb=[set.discard()]>, <Task pending name='Task-145' coro=<send_reminder() running at C:\Users\***\Nextcloud\P_Python\stfc_gebietsassistent\standalone_test.py:45> 
    ...gekürzt, das geht ewig so weiter ...

    Kann das mit deinem Hinweis in def on_ready() zusammen hängen?

    Quote
    Code
    #
    # FIXME There is no guarantee that this is called only once, so it must be
    #       guarded against subsequent calls.  After this is moved into a class.
    #


    Denke ich fahre mit der Subprocess Variante doch gar nicht so schlecht, ich werde jetzt dort mal deine angebrachten Verbesserungsvorschläge einarbeiten und hier dann nochmals präsentieren. Bei der Kombination seh ich mich nicht in der Lage die Probleme selbst zu beheben und ich möchte ja doch die Möglichkeit behalten, Probleme und Wartungen am Skript vornehmen zu können.

Participate now!

Don’t have an account yet? Register yourself now and be a part of our community!