discord.py no current event loop - python

I have this code which, in theory, will check for how many messages have been sent in a channel, and will do this at a certain time each day. Here is the code:
def checkTime():
# This function runs periodically every 1 second
threading.Timer(1, checkTime).start()
now = datetime.now()
current_time = now.strftime("%H:%M:%S")
print("Current Time =", current_time)
if(current_time == '21:35:20'): # check if matches with the desired time
print("starting")
channel = bot.get_channel(this is where I put the channel ID, just hiding it in here)
counter = 0
for message in channel.history():
counter += 1
print(counter)
checkTime()
However, once the time gets to 21:35:20, this error message appears:
starting
Exception in thread Thread-9:
Traceback (most recent call last):
File "C:\Users\jackt\AppData\Local\Programs\Python\Python39\lib\threading.py", line 954, in _bootstrap_inner
self.run()
File "C:\Users\jackt\AppData\Local\Programs\Python\Python39\lib\threading.py", line 1266, in run
self.function(*self.args, **self.kwargs)
File "C:\Users\jackt\Desktop\bot.py", line 51, in checkTime
for message in channel.history():
File "C:\Users\jackt\AppData\Local\Programs\Python\Python39\lib\site-packages\discord\abc.py", line 1087, in history
return HistoryIterator(self, limit=limit, before=before, after=after, around=around, oldest_first=oldest_first)
File "C:\Users\jackt\AppData\Local\Programs\Python\Python39\lib\site-packages\discord\iterators.py", line 256, in __init__
self.messages = asyncio.Queue()
File "C:\Users\jackt\AppData\Local\Programs\Python\Python39\lib\asyncio\queues.py", line 35, in __init__
self._loop = events.get_event_loop()
File "C:\Users\jackt\AppData\Local\Programs\Python\Python39\lib\asyncio\events.py", line 642, in get_event_loop
raise RuntimeError('There is no current event loop in thread %r.'
RuntimeError: There is no current event loop in thread 'Thread-9'.
I'm using "from datetime import datetime" and "import threading"
Please can you let me know what my code should look like to prevent this error. Thank you!

In General, with discord.py you should refrain from using threads at all possible as there is almost always an asyncio friendly way to accomplish the task.
As far as the reason for your problem is because asyncio is single-threaded the event loop that discord.py runs on is only on 1 thread, so when you try to access the event loop from another thread it will error because it assumes the event loop is on the same thread as you are calling from.
Now given this, asyncio does have threadsafe methods such as call_soon_threadsafe
But for this question specifically, discord.py has an extension dedicated to doing periodic tasks in the background: called the tasks extension: https://discordpy.readthedocs.io/en/latest/ext/tasks/index.html

Related

RuntimeError: There is no current event loop in thread 'MainThread'

I am currently working on a project where I am looking for information updates and then posting status messages to a slack channel. This is my first python project and I am a little out of the loop with what is going wrong. However, it appears that the RuntimeError: There is no current event loop in thread 'MainThread' error that I am getting is caused by having two async requests in my main function.
I was wondering if anyone would be able to tell me what best practice would be and how i could avoid any more issues?
def main():
configure()
print("the project has started")
asyncio.run(post_message("the project has started"))
event_filter = [my api call to another service goes here]
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(
asyncio.gather(
log_loop(event_filter, 2)))
finally:
# close loop to free up system resources
loop.close()
async def post_message(message):
try:
client = AsyncWebClient(token=os.getenv('SLACK_BOT_TOKEN'))
response = await client.chat_postMessage(channel='#notifications', text=message)
assert response["message"]["text"] == message
except SlackApiError as e:
assert e.response["ok"] is False
assert e.response["error"] # str like 'invalid_auth', 'channel_not_found'
print(f"Got an error: {e.response['error']}")
It seems to me that the call asyncio.run(post_message("the project has started")) is not playing well with my loop = asyncio.get_event_loop() but again I am unsure why.
Any help would be much appreciated!
EDIT
Here is the full traceback as requested:
Traceback (most recent call last):
File "/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.8/lib/python3.8/runpy.py", line 194, in _run_module_as_main
return _run_code(code, main_globals, None,
File "/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.8/lib/python3.8/runpy.py", line 87, in _run_code
exec(code, run_globals)
File "myprojectpath/__main__.py", line 4, in <module>
app.main()
File "myprojectpath/app.py", line 54, in main
loop = asyncio.get_event_loop()
File "/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.8/lib/python3.8/asyncio/events.py", line 639, in get_event_loop
raise RuntimeError('There is no current event loop in thread %r.'
RuntimeError: There is no current event loop in thread 'MainThread'.
This recreates your error:
import asyncio
async def test():
pass
def main():
asyncio.run(test())
loop = asyncio.get_event_loop()
loop.run_until_complete(test())
The issue is that asyncio.run creates an event loop, runs your coroutine, and then closes the event loop. Therefore you can't use that loop later on. Instead, change main into an async function and just use await OR use asyncio.run for your gather call. Try avoiding using the loop api unless you have to.

How to shutdown gracefully on keyboard interrupt when an asyncio task is performing _blocking_ work?

Update: asyncio simply does what it's told and you can handle these exceptions just fine - see my follow-up answer that I've marked as the solution to this question. Original question below, with slightly modified example to clarify the issue and its solution.
I've been trying to debug a library that I'm working on that relies heavily on asyncio. While working on some example code, I realised that performing a keyboard interrupt (CTRL-C) sometimes (rarely!) triggered the dreaded...
Task exception was never retrieved
I've tried hard to make sure that all tasks that I spin off handle asyncio.CancelledError gracefully, and after having spent way too many hours debugging this I realised that I only end up with this error message if one of the asyncio tasks is stuck on a blocking operation.
Blocking? You really shouldn't perform blocking work in tasks - that's why asyncio is kind enough to warn you about this. Run the below code...
import asyncio
from time import sleep
async def possibly_dangerous_sleep(i: int, use_blocking_sleep: bool = True):
try:
print(f"Sleep #{i}: Fine to cancel me within the next 2 seconds")
await asyncio.sleep(2)
if use_blocking_sleep:
print(
f"Sleep #{i}: Not fine to cancel me within the next 10 seconds UNLESS someone is"
" awaiting me, e.g. asyncio.gather()"
)
sleep(10)
else:
print(f"Sleep #{i}: Will sleep using asyncio.sleep(), nothing to see here")
await asyncio.sleep(10)
print(f"Sleep #{i}: Fine to cancel me now")
await asyncio.sleep(2)
except asyncio.CancelledError:
print(f"Sleep #{i}: So, I got cancelled...")
raise
def done_cb(task: asyncio.Task):
name = task.get_name()
try:
task.exception()
except asyncio.CancelledError:
print(f"Done: Task {name} was cancelled")
pass
except Exception as e:
print(f"Done: Task {name} didn't handle exception { e }")
else:
print(f"Done: Task {name} is simply done")
async def start_doing_stuff(collect_exceptions_when_gathering: bool = False):
tasks = []
for i in range(1, 7):
task = asyncio.create_task(
possibly_dangerous_sleep(i, use_blocking_sleep=True), name=str(i)
)
task.add_done_callback(done_cb)
tasks.append(task)
# await asyncio.sleep(3600)
results = await asyncio.gather(*tasks, return_exceptions=collect_exceptions_when_gathering)
if __name__ == "__main__":
try:
asyncio.run(start_doing_stuff(collect_exceptions_when_gathering=False), debug=True)
except KeyboardInterrupt:
print("User aborted through keyboard")
...and the debug console will tell you something along the lines of:
Executing <Task finished name='Task-2' coro=<possibly_dangerous_sleep() done, defined at ~/src/hej.py:5> result=None created at ~/.pyenv/versions/3.10.0/lib/python3.10/asyncio/tasks.py:337> took 10.005 seconds
Rest assured that the above call to sleep(10) isn't the culprit in the library I'm working on, but it illustrates the issue I'm running into: if I try to interrupt the above test application within the first 2 to 12 seconds of it running, the debug console will end up with a hefty source traceback:
Fine to cancel me within the next 2 seconds
Not fine to cancel me within the next 10 seconds UNLESS someone is awaiting me, e.g. asyncio.gather()
^CDone with: <Task finished name='Task-2' coro=<possibly_dangerous_sleep() done, defined at ~/src/hej.py:5> exception=KeyboardInterrupt() created at ~/.pyenv/versions/3.10.0/lib/python3.10/asyncio/tasks.py:337>
User aborted through keyboard
Task exception was never retrieved
future: <Task finished name='Task-2' coro=<dangerous_sleep() done, defined at ~/src/hej.py:5> exception=KeyboardInterrupt() created at ~/.pyenv/versions/3.10.0/lib/python3.10/asyncio/tasks.py:337>
source_traceback: Object created at (most recent call last):
File "~/.pyenv/versions/3.10.0/lib/python3.10/runpy.py", line 196, in _run_module_as_main
return _run_code(code, main_globals, None,
File "~/.pyenv/versions/3.10.0/lib/python3.10/runpy.py", line 86, in _run_code
exec(code, run_globals)
File "~/.vscode/extensions/ms-python.python-2021.12.1559732655/pythonFiles/lib/python/debugpy/__main__.py", line 45, in <module>
cli.main()
File "~/.vscode/extensions/ms-python.python-2021.12.1559732655/pythonFiles/lib/python/debugpy/../debugpy/server/cli.py", line 444, in main
run()
File "~/.vscode/extensions/ms-python.python-2021.12.1559732655/pythonFiles/lib/python/debugpy/../debugpy/server/cli.py", line 285, in run_file
runpy.run_path(target_as_str, run_name=compat.force_str("__main__"))
File "~/.pyenv/versions/3.10.0/lib/python3.10/runpy.py", line 269, in run_path
return _run_module_code(code, init_globals, run_name,
File "~/.pyenv/versions/3.10.0/lib/python3.10/runpy.py", line 96, in _run_module_code
_run_code(code, mod_globals, init_globals,
File "~/.pyenv/versions/3.10.0/lib/python3.10/runpy.py", line 86, in _run_code
exec(code, run_globals)
File "~/src/hej.py", line 37, in <module>
asyncio.run(start_doing_stuff(), debug=True)
File "~/.pyenv/versions/3.10.0/lib/python3.10/asyncio/runners.py", line 44, in run
return loop.run_until_complete(main)
File "~/.pyenv/versions/3.10.0/lib/python3.10/asyncio/base_events.py", line 628, in run_until_complete
self.run_forever()
File "~/.pyenv/versions/3.10.0/lib/python3.10/asyncio/base_events.py", line 595, in run_forever
self._run_once()
File "~/.pyenv/versions/3.10.0/lib/python3.10/asyncio/base_events.py", line 1873, in _run_once
handle._run()
File "~/.pyenv/versions/3.10.0/lib/python3.10/asyncio/events.py", line 80, in _run
self._context.run(self._callback, *self._args)
File "~/src/hej.py", line 28, in start_doing_stuff
task = asyncio.create_task(dangerous_sleep())
File "~/.pyenv/versions/3.10.0/lib/python3.10/asyncio/tasks.py", line 337, in create_task
task = loop.create_task(coro)
Traceback (most recent call last):
File "~/src/hej.py", line 37, in <module>
asyncio.run(start_doing_stuff(), debug=True)
File "~/.pyenv/versions/3.10.0/lib/python3.10/asyncio/runners.py", line 44, in run
return loop.run_until_complete(main)
File "~/.pyenv/versions/3.10.0/lib/python3.10/asyncio/base_events.py", line 628, in run_until_complete
self.run_forever()
File "~/.pyenv/versions/3.10.0/lib/python3.10/asyncio/base_events.py", line 595, in run_forever
self._run_once()
File "~/.pyenv/versions/3.10.0/lib/python3.10/asyncio/base_events.py", line 1873, in _run_once
handle._run()
File "~/.pyenv/versions/3.10.0/lib/python3.10/asyncio/events.py", line 80, in _run
self._context.run(self._callback, *self._args)
File "~/src/hej.py", line 14, in dangerous_sleep
sleep(10)
KeyboardInterrupt
If I replace await asyncio.sleep(3600) with await asyncio.gather(task) (see the example code) and invoke CTRL-C, I instead get a very neat shutdown sequence in my debug console:
Fine to cancel me within the next 2 seconds
Not fine to cancel me within the next 10 seconds UNLESS someone is awaiting me, e.g. asyncio.gather()
^CDone with: <Task finished name='Task-2' coro=<possibly_dangerous_sleep() done, defined at ~/src/hej.py:5> exception=KeyboardInterrupt() created at ~/.pyenv/versions/3.10.0/lib/python3.10/asyncio/tasks.py:337>
User aborted through keyboard
Can someone explain to me if this is by design? I was expecting all asyncio tasks to be cancelled for me when asyncio.run() was interrupted (while cleaning up after itself).
Summary: You need to handle your exceptions, or asyncio will complain.
For background tasks (i.e. tasks that you don't explicitly wait for using gather())
You might think that trying to catch cancellation using except asyncio.CancelledError (and re-raising it) within your task would handle all types of cancellation. That's not the case. If your task is performing blocking work while being cancelled, you won't be able to catch the exception (e.g. KeyboardInterrupt) within the task itself. The safe bet here is to register a done callback using add_done_callback on your asyncio.Task. In this callback, check if there was an exception (see the updated example code in the question). If your task was stuck on blocking work while being cancelled, the done callback will tell you that the task was done (vs cancelled).
For a bunch of tasks that you await using gather()
If you use gather, you don't need to add done callbacks. Instead, ask it to return any exceptions and it will handle KeyboardInterrupt just fine. If you don't do this, the first exception being raised within any of its awaitables is immediately propagated to the task that awaits on gather(). In the case of a KeyboardInterrupt inside a task that's stuck doing blocking work, KeyboardInterrupt will be re-raised and you'll need to handle it. Alternatively, use try/except to handle any exceptions raised. Please try this yourself by setting the collect_exceptions_when_gathering variable in the example code.
Finally: the only thing I don't understand now is that I don't see any exception being raised if one calls gather() with a single task, not asking it to return exceptions. Try to modify the example code to have its range be range(1,2) and you won't get a messy stack trace on CTRL-C...?

Cannot close a running event loop

What is the way to close the discord.py bot loop once tasks are done?
Added:
nest_asyncio.apply()
Tried:
bot.logout(), bot.close(), bot.cancel()
Code:
async def helper(bot, discord_user_feed):
nest_asyncio.apply()
await bot.wait_until_ready()
# await asyncio.sleep(10)
for each_id, events_list in discord_user_feed.items():
discord_user = await bot.fetch_user(int(each_id))
for each_one in events_list:
msg_sent = True
while msg_sent:
await discord_user.send(each_one)
msg_sent = False
await bot.close()
return 'discord messages sent'
async def discord_headlines(request):
nest_asyncio.apply()
discord_user_feed = await get_details()
bot = commands.Bot(command_prefix='!')
bot.loop.create_task(helper(bot, discord_user_feed))
message = bot.run('my_token')
return HttpResponse(message)
I can be able to send messages to discord users using id and discord bot. But, even after, django view api is continuously running and getting the error. It needs to return a response message - discord messages sent.
Error:
Exception in callback <TaskWakeupMethWrapper object at 0x000001C852D2DA38>(<Future finis...SWdZVtXT7E']}>)
handle: <Handle <TaskWakeupMethWrapper object at 0x000001C852D2DA38>(<Future finis...SWdZVtXT7E']}>)>
Traceback (most recent call last):
File "E:\sd\envs\port\lib\asyncio\events.py", line 145, in _run
self._callback(*self._args)
KeyError: <_WindowsSelectorEventLoop running=True closed=False debug=False>
Internal Server Error: /api/v0/discord_headlines/
Traceback (most recent call last):
File "E:\sd\envs\port\lib\site-packages\django\core\handlers\exception.py", line 47, in inner
response = get_response(request)
File "E:\sd\envs\port\lib\site-packages\django\core\handlers\base.py", line 179, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "E:\sd\envs\port\lib\site-packages\asgiref\sync.py", line 139, in __call__
return call_result.result()
File "E:\sd\envs\port\lib\concurrent\futures\_base.py", line 425, in result
return self.__get_result()
File "E:\sd\envs\port\lib\concurrent\futures\_base.py", line 384, in __get_result
raise self._exception
File "E:\sd\envs\port\lib\site-packages\asgiref\sync.py", line 204, in main_wrap
result = await self.awaitable(*args, **kwargs)
File "E:\back-end\port_api\views\discord_integration.py", line 110, in discord_headlines
message = bot.run('my_token')
File "E:\sd\envs\port\lib\site-packages\discord\client.py", line 719, in run
_cleanup_loop(loop)
File "E:\sd\envs\port\lib\site-packages\discord\client.py", line 95, in _cleanup_loop
loop.close()
File "E:\sd\envs\port\lib\asyncio\selector_events.py", line 107, in close
raise RuntimeError("Cannot close a running event loop")
RuntimeError: Cannot close a running event loop
[13/Jun/2021 01:07:34] "GET /api/v0/discord_headlines/ HTTP/1.1" 500 109010
These are the versions:
Python 3.6.4, Django 3.1, discord.py 1.7.3, Asyncio, Anaconda-4.9.2, Windows-10
I've not used older versions so I can't attest to the accuracy of this answer for pre-2021 releases (v1.6.0+) Discord.py.
When starting a discord Client or Bot via run(*args, **kwargs) there is actually no class method to stop it.
Per the Discord.py Documentations Discord.py discord.Client.run:
A blocking call that abstracts away the event loop initialisation from
you.
If you want more control over the event loop then this function should
not be used. Use start() coroutine or connect() + login()
And it's effectively equal to:
# run(*args, **kwargs) is basically
try:
# Start the bot on asyncio event loop with using .start() coroutine
loop.run_until_complete(start(*args, **kwargs))
except KeyboardInterrupt:
# Invoke close() coroutine on the same loop if interrupt
loop.run_until_complete(close())
# Cancel any other lingering tasks and such
finally:
loop.close()
So the unrewarding but accurate answer with your current attempted implementation is simply... you can't.
If you wanna drop some more info on the webserver library your using in response or via a message, I can help craft you something functional.
If your webserver is fully asyncio integrated, your best bet is seeing if the library has a way to add tasks to its existing event loop, or if you pass one to it. Sanic allows for this in its decorator startup/shutdown methods (ex. after_server_start, etc). Store the reference to the bot instance somewhere and pass it around as needed.
I'll try and keep an eye on here for a response or DM and try to help get you functional and we can post updated code answer here, there's a big lack of solid answers for using discord.py in more complex cases within other applications/webservices and I just ran into a similiar issue with writing a discord.py bot that accepted webhook data and posted embeds to appropriate channels based on some filters.

How to use telethon in a thread

I want to run a function in background. so I use Threading in my code.
but return error ValueError: signal only works in main thread and don't know about two things:
what is the main thread
how to solve this problem :)
views.py
def callback(update):
print('I received', update)
def message_poll_start():
try:
client = TelegramClient('phone', api_id, api_hash,
update_workers=1, spawn_read_thread=False)
client.connect()
client.add_update_handler(callback)
client.idle()
except TypeNotFoundError:
pass
def message_poll_start_thread(request):
t = threading.Thread(target=message_poll_start, args=(), kwargs={})
t.setDaemon(True)
t.start()
return HttpResponse("message polling started")
urls.py
urlpatterns = [
path('message_poll_start', messagemanager_views.message_poll_start_thread, name="message_poll_start"),
]
trace
[12/Jan/2018 11:24:38] "GET /messages/message_poll_start HTTP/1.1" 200 23
Exception in thread Thread-3:
Traceback (most recent call last):
File "/usr/lib/python3.5/threading.py", line 914, in _bootstrap_inner
self.run()
File "/usr/lib/python3.5/threading.py", line 862, in run
self._target(*self._args, **self._kwargs)
File "/home/teletogram/telethogram/messagemanager/views.py", line 123, in message_poll_start
client0.idle()
File "/home/teletogram/.env/lib/python3.5/site-packages/telethon/telegram_bare_client.py", line 825, in idle
signal(sig, self._signal_handler)
File "/usr/lib/python3.5/signal.py", line 47, in signal
handler = _signal.signal(_enum_to_int(signalnum), _enum_to_int(handler))
ValueError: signal only works in main thread
1) A python script runs in the main thread by default. If you spawn a new thread using threading.Thread, that will create a new thread which runs separately from the main one. When I began learning about threading I spent a lot of time reading before it started to click. The official threading docs are decent for basic functionality, and I like this tutorial for a deeper dive.
2) The internals of Telethon rely on asyncio. In asyncio each thread needs its own asynchronous event loop, and thus spawned threads need an explicitly created event loop. Like threading, asyncio is a large topic, some of which is covered in the Telethon docs.
Something like this should work:
import asyncio
def message_poll_start():
try:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
client = TelegramClient('phone', api_id, api_hash, loop=loop)
client.connect()
client.add_update_handler(callback)
client.idle()
except TypeNotFoundError:
pass

Calling python asyncio loop.run_until_complete() with discord.py not working?

Making a Discord bot using discord.py, this is the first time I work with asyncio, and probably the first time I encountered something this frustrating in Python.
The point of this question isn't to teach me how to use asyncio, but instead to teach me how to avoid using it, even if it's not the right way to do things.
So I needed to run the discord client coroutines from regular def functions. After hours of searching I found this: asyncio.get_event_loop().run_until_complete(...). I set up a small script to test it out:
import asyncio
async def test():
print('Success')
asyncio.get_event_loop().run_until_complete(test())
And it worked perfectly. So I went ahead and tried to use it in a discord bot:
import discord
import asyncio
client = discord.Client()
#client.event
async def on_ready():
test()
def test():
asyncio.get_event_loop().run_until_complete(run())
async def run():
print('Success')
client.run('TOKEN_HERE')
And I got an error... Stacktrace/Output:
Success
Ignoring exception in on_ready
Traceback (most recent call last):
File "C:\Program Files\Python36\lib\site-packages\discord\client.py", line 307, in _run_event
yield from getattr(self, event)(*args, **kwargs)
File "C:/Users/OverclockedSanic/PyCharm Projects/asyncio test/test.py", line 8, in on_ready
test()
File "C:/Users/OverclockedSanic/PyCharm Projects/asyncio test/test.py", line 11, in test
asyncio.get_event_loop().run_until_complete(run())
File "C:\Program Files\Python36\lib\asyncio\base_events.py", line 454, in run_until_complete
self.run_forever()
File "C:\Program Files\Python36\lib\asyncio\base_events.py", line 408, in run_forever
raise RuntimeError('This event loop is already running')
RuntimeError: This event loop is already running
What's weird is that "Success" part at the end... I tried some other tests to see if I could return data from the coroutine or execute more stuff, but it couldn't.
I even tried replacing asyncio.get_event_loop() with client.loop, which didn't work either.
I looked for like 2 days, still no solution. Any ideas?
EDIT: Replacing get_event_loop() with new_event_loop() as suggested in the comments raised this:
Ignoring exception in on_ready
Traceback (most recent call last):
File "C:\Program Files\Python36\lib\site-packages\discord\client.py", line 307, in _run_event
yield from getattr(self, event)(*args, **kwargs)
File "C:/Users/USER/PyCharm Projects/asyncio test/test.py", line 8, in on_ready
test()
File "C:/Users/USER/PyCharm Projects/asyncio test/test.py", line 11, in test
asyncio.new_event_loop().run_until_complete(run())
File "C:\Program Files\Python36\lib\asyncio\base_events.py", line 454, in run_until_complete
self.run_forever()
File "C:\Program Files\Python36\lib\asyncio\base_events.py", line 411, in run_forever
'Cannot run the event loop while another loop is running')
RuntimeError: Cannot run the event loop while another loop is running
Your problem seems to essentially be about mixing synchronous and asynchronous code. There are two possibilities:
1) If your non-async routines don't need to block, just to schedule some async task (e.g. send_message) to be run later, then they can simply call get_event_loop().create_task(). You can even use add_done_callback on the returned task if you want some other (non-async) routine to be called when the asynchronous operation is complete. (If the routine to be run is also non-async, then use get_event_loop().call_soon().)
2) If your non-async routines absolutely must block (which includes possibly awaiting an asynchronous routine), and cannot schedule the blocking operation for later, then you should not run them from the same thread as the main event loop. You can create a thread pool with concurrent.futures.ThreadPoolExecutor, and use asyncio.run_in_executor() to schedule your non-async routines, then await the result. And if they in turn need to call async routines, then run_until_complete() should work because now you're not running in a thread that already has an event loop. (But beware of threadsafety issues. You may need something like run_coroutine_threadsafe if you need to wait for something to run in the main event loop.)
If it helps, the asgiref package contains routines that can simplify this for you. They're designed for a slightly different purpose (web servers), but may also work for you. You can use await asgiref.sync.sync_to_async(func)(args) when you want to call a non-async routine from an async routine, which will run the routine in a thread pool, then use asgiref.sync.async_to_sync(func)(args) when you want to call an async routine from a non-async routine that's running inside that thread pool.

Categories