3

I have a fastAPI project that talks to a Cosmos database. All of my fastAPI routes are async (async def ...). I need an async class that will perform CRUD operations of the Cosmos DB. The problem I'm having is figuring out a constructor for the class.

I want the constructor to:

  • Take in the CosmosClient as an arg (azure.cosmos.aio.CosmosClient)
  • Use the client to make sure the database is created
  • Use the client to make sure the container is created

Something like this:

class CosmosCRUD:
    def __init__(self, client: CosmosClient):
        self.client = CosmosClient
        self.database = await self.client.create_database_if_not_exists("MY_DATABASE_NAME")
        self.container = await self.database.create_container_if_not_exists("MY_CONTAINER_NAME", partition_key=...)

Unfortunately, you can only await inside of async functions, and __init__ can't be async, so the code above doesn't work.

As far as I can tell, there's a few solutions:

  • Create a new event loop inside of __init__ and run the async code that gets the db and container within that
    • As far as I can tell, that means that fastAPI will stop processing all requests any time an instance of this class is being created, which is probably going to be on every request. This will absolutely destroy performance of the entire app
  • Ignore __init__ and create a new async constructor function like create that users of this class need to call
    • The IDE (PyCharm) doesn't know that create is the constructor that will always be called first. Any instance variables created in this new constructor that you try to use in other methods will be flagged as non-existing by the IDE, because they weren't created in __init__
    • I tried having both a create async classmethod that is the actual constructor, and an __init__ method where I just added declarations for any instance variables created in create, with the types (ex: self.client: CosmosClient) but the IDE still complains they don't exist
  • Use dependency injection somehow
    • Something like this: https://stackoverflow.com/a/65247387/6423456
    • Unfortunately, my __init__ method is taking the client as an argument, and needs to pass it to the function being injected
    • In the example referenced above, that would be like needing to pass the a arg to the async_dep being injected.
    • Is that even possible? I'm guessing not

Is there a good way to have async code inside of a constructor, without making the IDE unhappy about missing instance variables?

3
  • You can have async __init__ if you want, there are several ways for doing so, I mentioned one here. Commented Jan 6, 2024 at 20:02
  • the below answer should help stackoverflow.com/questions/33128325/… Commented Jan 31 at 14:44
  • This question is similar to: How to set class attribute with await in __init__. If you believe it’s different, please edit the question, make it clear how it’s different and/or how the answers on that question are not helpful for your problem. Commented Jan 31 at 14:45

2 Answers 2

1

You can inherit your class from AsyncMixin and implement __ainit__ method, there you can use async functions:

import asyncio


class AsyncMixin:
    def __init__(self, *args, **kwargs):
        """
        Standard constructor used for arguments pass
        Do not override. Use __ainit__ instead
        """
        self.__storedargs = args, kwargs
        self.async_initialized = False

    async def __ainit__(self, *args, **kwargs):
        """Async constructor, you should implement this"""

    async def __initobj(self):
        """Crutch used for __await__ after spawning"""
        assert not self.async_initialized
        self.async_initialized = True
        # pass the parameters to __ainit__ that passed to __init__
        await self.__ainit__(*self.__storedargs[0], **self.__storedargs[1])
        return self

    def __await__(self):
        return self.__initobj().__await__()


class CosmosClient():
    pass


class CosmosCRUD(AsyncMixin):
    def __init__(self, client: CosmosClient):
        self.client = client
        super().__init__()    

    async def __ainit__(self):
        await asyncio.sleep(1)
        print("it works!")
        # self.database = await self.client.create_database_if_not_exists("MY_DATABASE_NAME")
        # self.container = await self.database.create_container_if_not_exists("MY_CONTAINER_NAME", partition_key=...)

    def some_method(self):
        assert self.async_initialized == True



async def main():
    c = await CosmosCRUD(client=CosmosClient())
    c.some_method()

    cc = CosmosCRUD(client=CosmosClient())
    cc.some_method() # Will fail, because it was not awaited during the instance construction


if __name__ == "__main__":
    asyncio.run(main())

Sign up to request clarification or add additional context in comments.

1 Comment

Elegant! love it!, I just to be more "pythonic" will remove __initobj and apply names like __async_initialized__ and __asyncinit__ to follow the rules or replace the property for a dict inside the module with instance as key to not use an instance property
1

It is generally a bad idea to run asyc code inside a constructor. You should consider a redesign of your program flow. Classes should be trivially constructible. Instead consider a init, run and done kind of program flow and put your logic inside the run part.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.