Python Asyncio schedule tasks weekly - python

I am writing a discord bot in Python, which already has the two async overwritten methods on_ready and on_message to react to messages in a certain way.
Now I wanted to add a function, which should be called once in a week. I tried something with asyncio.sleep() but I don't want to start the bot at that specific time and then sleep 604.800 sec (1 week in seconds) to repeat that function every week.
Here's what I got so far:
class MyClient(discord.Client):
async def on_ready(self):
#some initial stuff
self.loop.create_task(self.routine())
async def on_message(self, message):
#reply to chat messages
await message.channel.send("Reply")
async def routine(self):
while True:
print("I'm on that routine boi")
await asyncio.sleep(3)
It works so far, that I can use the bot in discord and get the outputs every 3 seconds.
But I'd prefer something like from the schedule module to use something like
scheduler.every().sunday.at("8:55").do(self.routine())
Any chance to do something similar in combination with asyncio?

The Event loop provides mechanisms to schedule callback functions to be called at some point in the future, as shown here in Python docs
class MyClient(discord.Client):
async def on_ready(self):
#some initial stuff
self.loop.create_task(self.routine())
async def on_message(self, message):
#reply to chat messages
await message.channel.send("Reply")
async def execute():
res = await loop.call_later(delay, callback, *args)
delay += delay + 604800
return res
async def routine(self):
while True:
print("I'm on that routine boi")
res = await loop.run_until_complete(execute)

Related

Pass async function (changes channel name) into thread

I'm trying to pass an async function into a #tasks.loop thread since my commands don't work until the task is finished, if threading isn't used. However the task I'm trying to do every 5 minutes is to change the channel name, and it doesn't work. I get a Timeout context manager should be used error.
async def change_name()
channel = client.get_channel(chid)
await chid.edit(name='yes')
#tasks.loop(minutes=5)
async def loop():
_thread = threading.Thread(target=asyncio.run,args=())
_thread.start()
loop.start()
You can try python-worker for it (link)
from worker import async_worker
#async_worker
async def change_name()
channel = client.get_channel(chid)
await chid.edit(name='yes')
#tasks.loop(minutes=5)
async def loop():
await change_name()
loop.start()
your change_name will be run as thread automatically

Alternative to asyncio.gather which I can keep adding coroutines to at runtime?

I need to be able to keep adding coroutines to the asyncio loop at runtime. I tried using create_task() thinking that this would do what I want, but it still needs to be awaited.
This is the code I had, not sure if there is a simple edit to make it work?
async def get_value_from_api():
global ASYNC_CLIENT
return ASYNC_CLIENT.get(api_address)
async def print_subs():
count = await get_value_from_api()
print(count)
async def save_subs_loop():
while True:
asyncio.create_task(print_subs())
time.sleep(0.1)
async def start():
global ASYNC_CLIENT
async with httpx.AsyncClient() as ASYNC_CLIENT:
await save_subs_loop()
asyncio.run(start())
I once created similar pattern when I was mixing trio and kivy, which was demonstration of running multiple coroutines asynchronously.
It use a trio.MemoryChannel which is roughly equivalent to asyncio.Queue, I'll just refer it as queue here.
Main idea is:
Wrap each task with class, which has run function.
Make class object's own async method to put object itself into queue when execution is done.
Create a global task-spawning loop to wait for the object in queue and schedule execution/create task for the object.
import asyncio
import traceback
import httpx
async def task_1(client: httpx.AsyncClient):
resp = await client.get("http://127.0.0.1:5000/")
print(resp.read())
await asyncio.sleep(0.1) # without this would be IP ban
async def task_2(client: httpx.AsyncClient):
resp = await client.get("http://127.0.0.1:5000/meow/")
print(resp.read())
await asyncio.sleep(0.5)
class CoroutineWrapper:
def __init__(self, queue: asyncio.Queue, coro_func, *param):
self.func = coro_func
self.param = param
self.queue = queue
async def run(self):
try:
await self.func(*self.param)
except Exception:
traceback.print_exc()
return
# put itself back into queue
await self.queue.put(self)
class KeepRunning:
def __init__(self):
# queue for gathering CoroutineWrapper
self.queue = asyncio.Queue()
def add_task(self, coro, *param):
wrapped = CoroutineWrapper(self.queue, coro, *param)
# add tasks to be executed in queue
self.queue.put_nowait(wrapped)
async def task_processor(self):
task: CoroutineWrapper
while task := await self.queue.get():
# wait for new CoroutineWrapper Object then schedule it's async method execution
asyncio.create_task(task.run())
async def main():
keep_running = KeepRunning()
async with httpx.AsyncClient() as client:
keep_running.add_task(task_1, client)
keep_running.add_task(task_2, client)
await keep_running.task_processor()
asyncio.run(main())
Server
import time
from flask import Flask
app = Flask(__name__)
#app.route("/")
def hello():
return str(time.time())
#app.route("/meow/")
def meow():
return "meow"
app.run()
Output:
b'meow'
b'1639920445.965701'
b'1639920446.0767004'
b'1639920446.1887035'
b'1639920446.2986999'
b'1639920446.4067013'
b'meow'
b'1639920446.516704'
b'1639920446.6267014'
...
You can see tasks running repeatedly on their own pace.
Old answer
Seems like you only want to cycle fixed amount of tasks.
In that case just iterate list of coroutine with itertools.cycle
But this is no different with synchronous, so lemme know if you need is asynchronous.
import asyncio
import itertools
import httpx
async def main_task(client: httpx.AsyncClient):
resp = await client.get("http://127.0.0.1:5000/")
print(resp.read())
await asyncio.sleep(0.1) # without this would be IP ban
async def main():
async with httpx.AsyncClient() as client:
for coroutine in itertools.cycle([main_task]):
await coroutine(client)
asyncio.run(main())
Server:
import time
from flask import Flask
app = Flask(__name__)
#app.route("/")
def hello():
return str(time.time())
app.run()
Output:
b'1639918937.7694323'
b'1639918937.8804302'
b'1639918937.9914327'
b'1639918938.1014295'
b'1639918938.2124324'
b'1639918938.3204308'
...
asyncio.create_task() works as you describe it. The problem you are having here is that you create an infinite loop here:
async def save_subs_loop():
while True:
asyncio.create_task(print_subs())
time.sleep(0.1) # do not use time.sleep() in async code EVER
save_subs_loop() keeps creating tasks but control is never yielded back to the event loop, because there is no await in there. Try
async def save_subs_loop():
while True:
asyncio.create_task(print_subs())
await asyncio.sleep(0.1) # yield control back to loop to give tasks a chance to actually run
This problem is so common I'm thinking python should raise a RuntimeError if it detects time.sleep() within a coroutine :-)
You might want to try the TaskThread framework
It allows you to add tasks in runtime
Tasks are re-scheduled periodically (like in your while loop up there)
There is a consumer / producer framework built in (parent/child relationships) which you seem to need
disclaimer: I wrote TaskThread out of necessity & it's been a life saver.

SyntaxError: 'await' outside function | discord.py [duplicate]

This question already has an answer here:
SyntaxError (Python): 'await' outside async function
(1 answer)
Closed 1 year ago.
I know - there are a lot of questions with this title, but most are for Dark Sky or are different to my problem.
class Counter(discord.ui.View):
#discord.ui.button(label='0', style=discord.ButtonStyle.red)
async def counter(self, button: discord.ui.Button, interaction:
discord.Interaction):
number = int(button.label)
button.label = str(number + 1)
if number + 1 >= 5:
button.style = discord.ButtonStyle.green
await interaction.message.edit(view=self)
view = Counter()
await ctx.send('Press to increment', view=view)
Error message:
File "main.py", line 27
await ctx.send('Press to increment', view=view)
^
SyntaxError: 'await' outside function
I'm very new to python and am not sure why this is at all. Any help would be greatly appreciated!
you need to put the
view = Counter()
await ctx.send('Press to increment', view=view)
code inside a asynchron function, because the await keyword can only be used inside of asynchron functions.
example functions
async def my_function(): # this is an async function
...
def my_other_function(): # this is a normal function
...
Async functions have to be called with the await keyword, or else you'll get an exception
await my_function() # calling an async function
my_other_function() # calling a normal function
as you can see in this example, in the async function, you can use await
async def something():
print("hello async world")
async def my_function():
await something()
but if you try to await an async function inside a normal function
asnyc def something():
print("hello async world")
def my_other_function():
await something()
You will get the SyntaxError: 'await' outside function error
The best way to do this together with a discord bot is by adding the code to a listener. For example with the on_message event which will be fired whenever a message is incoming
Example for on_message event
import discord
client = discord.Client()
#client.event
async def on_message(message): # The event
if message.content == "your_text" # Insert a text here that is needed to use the "command"
# your code will be executed whenever the bot receives a message with the content "your_text"
view = Counter()
await message.channel.send('Press to increment', view=view)
client.run("your unique token here")
I would recommend to take a look at https://discordpy.readthedocs.io/en/stable/quickstart.html before starting
According to the ctx, your code is made for the client's #commands.command function.
(this should fix your code)
from discord.ext import commands
bot = commands.Bot(command_prefix="!")
class Counter(discord.ui.View):
#discord.ui.button(label='0', style=discord.ButtonStyle.red)
async def counter(self, button: discord.ui.Button, interaction:
discord.Interaction):
number = int(button.label)
button.label = str(number + 1)
if number + 1 >= 5:
button.style = discord.ButtonStyle.green
await interaction.message.edit(view=self)
#bot.command("test")
async def command(ctx):
view = Counter()
await ctx.send('Press to increment', view=view)
bot.run("your unique token here")
In order to use your code, you need to send a message in a channel where your bot can read messages with the content !test to invoke the command
The typical structure of your code will be (using sample code from the docs)
#bot.command()
async def foo(ctx, arg):
await ctx.send(arg)
That's also where the ctx you're using comes from--if you removed the await you'd get an error that ctx wasn't defined. Your code will specify functions that correspond to commands or other events on discord, and discord.py will handle calling the functions you specify when users perform those actions.
You can only use the await keyword in async functions. The idea behind these functions is that if your code has to do something that takes a long time, but mostly just involves waiting, python can go do other stuff while it waits. For discord.py, since a lot of your operations involve waiting for slow network operations like sending messages, that's very important.
here's one intro to the ideas behind async/await, I haven't read it too much, so if it's bad or anybody has better suggestions drop a link in the comments.

python Make an async timer without waiting to finish

I want to make a timer which is started in a normal function, but in the timer function, it should be able to call an async function
I want to do something like this:
startTimer()
while True:
print("e")
def startTimer(waitForSeconds: int):
# Wait for `waitForSeconds`
await myAsyncFunc()
async def myAsyncFunc():
print("in my async func")
Where the while True loop should do its stuff and after waitForSeconds the timer the async function should execute an other async function, but waiting shouldn't block any other actions and doesn't need to be awaited
If something isn't understandable, I'm sorry, I'll try to explain it then
Thanks
If you want to run your synchronous and asynchronous code in parallel, you will need to run one of them in a separate thread. For example:
def sync_code():
while True:
print("e")
async def start_timer(secs):
await asyncio.sleep(secs)
await async_func()
async def main():
asyncio.create_task(start_timer(1))
loop = asyncio.get_event_loop()
# use run_in_executor to run sync code in a separate thread
# while this thread runs the event loop
await loop.run_in_executor(None, sync_code)
asyncio.run(main())
If the above is not acceptable for you (e.g. because it turns the whole program into an asyncio program), you can also run the event loop in a background thread, and submit tasks to it using asyncio.run_coroutine_threadsafe. That approach would allow startTimer to have the signature (and interface) like you wanted it:
def startTimer(waitForSeconds):
loop = asyncio.new_event_loop()
threading.Thread(daemon=True, target=loop.run_forever).start()
async def sleep_and_run():
await asyncio.sleep(waitForSeconds)
await myAsyncFunc()
asyncio.run_coroutine_threadsafe(sleep_and_run(), loop)
async def myAsyncFunc():
print("in my async func")
startTimer(1)
while True:
print("e")
I'm pretty sure that you are familiar with concurent processing, but you didn't show exactly what you want. So if I understand you correctly you want to have 2 processes. First is doing only while True, and the second process is the timer(waits e.g. 5s) and it will call async task. I assume that you are using asyncio according to tags:
import asyncio
async def myAsyncFunc():
print("in my async func")
async def call_after(delay):
await asyncio.sleep(delay)
await myAsyncFunc()
async def while_true():
while True:
await asyncio.sleep(1) # sleep here to avoid to large output
print("e")
async def main():
task1 = asyncio.create_task(
while_true())
task2 = asyncio.create_task(
call_after(5))
# Wait until both tasks are completed (should take
# around 2 seconds.)
await task1
await task2
asyncio.run(main())

How do I run multiple functions at a time in discord.py?

In my bot, I have a function that consistently adds to values in a dictionary once a second to represent "paying employees". I also have a #client.command function that shows the keys and values of the dictionary at the time that the command was sent. While the "paying" function is on though (always), the look-up function doesn't work, the on_message function does. How do I make it so the bot will detect #client.command function in parallel?
import discord
from discord.ext import commands
from discord.ext import tasks
import asyncio
client = commands.Bot(command_prefix='/')
payroll = []
data = {}
# /start will start operations and manage payroll for everyone on the server
# will make it on_ready
#client.event
async def on_ready():
global data
server = client.get_guild(670006903815929889)
# starts balance for everyone on the server at the time of the command and adds them to the payroll
for person in server.members:
person = str(person)
payroll.append(person)
data[person] = int(0)
print(data)
await asyncio.sleep(1)
# pays everyone at a steady BASE rate
while True:
for person in payroll:
data[person] += 10
print(data)
await asyncio.sleep(60)
# on message
#client.event
async def on_message(ctx):
global data
balance = int(data[str(ctx.author)])
data[str(ctx.author)] -= 5
if balance <= 25 and ctx.author.id != 734119994761150585:
await ctx.channel.send('Be careful {author}! Your balance is ${balance}.')
if balance <= 0 and ctx.author.id != 734119994761150585:
# delete message
await ctx.channel.send('Oops! {author}, you have run out of money. As a consequence, you will be deducted $15.')
data[str(ctx.author)] -= 15
if message.startswith("/dm"):
await message.author.send('Hello World!')
# look-up stats
#client.command()
async def dm(message):
await message.author.send('stats go here')
client.run("token")
The command function will not work as you are not processing the command.
To do that, add the small line in your message function:
#client.event()
async def on_message(message):
#YOUR CODE HERE
await client.process_commands(message)
For running the function constantly or running multiple at a time:
You can add the tasks in the function which will run constantly or at the given time difference.
The in-depth document can be found in the discord.py document.
The snippet of a Code:
from discord.ext import tasks
#tasks.loop(seconds = 5)
async def test_function():
#YOUR CODE
pass
#client.event
async def on_ready():
#TASK RUN
test_function.start()

Categories