From ee92e77112a7e4aa55da1abec5f2d1d7707a419f Mon Sep 17 00:00:00 2001 From: winds0r Date: Sun, 16 Oct 2022 19:56:06 +0900 Subject: [PATCH 1/5] (add/docs)django/drf cookbook specifies how to setup rocketry in a django/drf app, per recommandations seen in #76. also adds a little note about django's async ORM, as I've personnally had issues making it work at first, so it might be helpful to others as well. --- docs/cookbook/django.rst | 121 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 docs/cookbook/django.rst diff --git a/docs/cookbook/django.rst b/docs/cookbook/django.rst new file mode 100644 index 00000000..55bab1e4 --- /dev/null +++ b/docs/cookbook/django.rst @@ -0,0 +1,121 @@ + +Integrate Django (or Django Rest Framework) +=========================================== + +This cookbook will use DRF (Django REST Framework) as an example, but the syntax is exaclty +the same for Django. + +First, let's create a new command for setting up our jobs (we will call it `inittasks.py`): + +.. code-block:: python + + # Create Rocketry app + from rocketry import Rocketry + app = Rocketry(config={"task_execution": "async"}) + + + # Create some tasks + + @app.task('every 5 seconds') + async def do_things(): + ... + + class Command(BaseCommand): + help = "Setup the periodic tasks runner" + + def handle(self, *args, **options): + app.run() + +You can also update your `wsgi` file, but this is not the recommended way, as it will not work +if you use Gunicorn or Uvicorn, or any other HTTP server. + +Next, you can just run in your `entrypoint` script (or in your shell) the following command: + +.. code-block:: bash + + ... # connect to the database, set PRODUCTION=1, etc... + + # if you deploy on Docker, it's probably better to run this in the background, + # so you can run your HTTP server afterwards + python manage.py inittasks & + + +And you're set ! It's really as simple as that! + +Now, you might have the following error if you use Querysets, or try to use the ORM in your task: + +.. code-block:: python + + Django: SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async + +Some guides might recommend to set `DJANGO_ALLOW_ASYNC_UNSAFE` to True in your shell before running the tasks. +This is not the recommended way. + +For our example, we will use the same file for simplicity, but you can move it to another file, of course. + +.. code-block:: python + + # Create Rocketry app + from rocketry import Rocketry + app = Rocketry(config={"task_execution": "async"}) + + + # this is a synchronous operation that will work in an async context! + def do_things_with_users(name): + for user in User.objects.filter(name=name): + print(f'I did something with {user} !') + + @app.task('every 5 seconds') + async def do_things(): + await sync_to_async(do_things_with_users)(name='John Doe') + ... + + class Command(BaseCommand): + help = "Setup the periodic tasks runner" + + def handle(self, *args, **options): + app.run() + +You can even manually run the `do_things_with_users` function from a view now, +if that's something you would want. Let's add this view in our `views.py` file: + +.. code-block:: python + + from api.commands import inittasks as tasklist + + class TaskView(APIView): + def get(self, request): + + """ + This function is not ran by our scheduler, and runs in a synchronous context in our example + """ + + name = request.GET.get('name') + if not name : + return Response({ + 'error': 'missing a parameter (expected something like ' + '?name=job_name )' + }, status=HTTP_400_BAD_REQUEST) + + try: + + task = getattr(tasklist, name) + + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + loop.run_until_complete(task()) + + except Exception as err: + + return Response({ + 'error': 'task failure', + 'logs': f'Failed with: {str(err)}', + }, status=HTTP_500_INTERNAL_SERVER_ERROR) + + return Response({ + 'message': 'successfully ran the task', + }, status=HTTP_200_OK) + + +Note that you will only need to use `sync_to_async` if you use the asynchronous ORM. From 5474085d2584a8a0380aab2e96fc12d66e98976d Mon Sep 17 00:00:00 2001 From: winds0r Date: Sun, 16 Oct 2022 19:59:39 +0900 Subject: [PATCH 2/5] (mod/docs)adding `import asyncio` might make the docs a bit clearer --- docs/cookbook/django.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/cookbook/django.rst b/docs/cookbook/django.rst index 55bab1e4..b011e22f 100644 --- a/docs/cookbook/django.rst +++ b/docs/cookbook/django.rst @@ -82,6 +82,9 @@ if that's something you would want. Let's add this view in our `views.py` file: .. code-block:: python from api.commands import inittasks as tasklist + import asyncio + + ... class TaskView(APIView): def get(self, request): From 00602e1dd69ecbc903d5e1f7e307f10bdb8533e1 Mon Sep 17 00:00:00 2001 From: winds0r Date: Mon, 17 Oct 2022 14:33:57 +0900 Subject: [PATCH 3/5] (mod/docs)improving django docs - makes it look better overall, and easier to read (using subtitles and all that). also fixes ``code_examples`` - calls it from the cookbook index, so that it can actually be reached by a user --- docs/cookbook/django.rst | 28 +++++++++++++++++----------- docs/cookbook/index.rst | 3 ++- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/docs/cookbook/django.rst b/docs/cookbook/django.rst index b011e22f..b7def722 100644 --- a/docs/cookbook/django.rst +++ b/docs/cookbook/django.rst @@ -5,7 +5,7 @@ Integrate Django (or Django Rest Framework) This cookbook will use DRF (Django REST Framework) as an example, but the syntax is exaclty the same for Django. -First, let's create a new command for setting up our jobs (we will call it `inittasks.py`): +First, let's create a new command for setting up our jobs (we will call it ``inittasks.py``): .. code-block:: python @@ -26,10 +26,8 @@ First, let's create a new command for setting up our jobs (we will call it `init def handle(self, *args, **options): app.run() -You can also update your `wsgi` file, but this is not the recommended way, as it will not work -if you use Gunicorn or Uvicorn, or any other HTTP server. -Next, you can just run in your `entrypoint` script (or in your shell) the following command: +Next, you can just run in your ``entrypoint`` script (or in your shell) the following command: .. code-block:: bash @@ -42,16 +40,22 @@ Next, you can just run in your `entrypoint` script (or in your shell) the follow And you're set ! It's really as simple as that! +Using the Asynchronous ORM +-------------------------- + Now, you might have the following error if you use Querysets, or try to use the ORM in your task: -.. code-block:: python +.. code-block:: bash Django: SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async -Some guides might recommend to set `DJANGO_ALLOW_ASYNC_UNSAFE` to True in your shell before running the tasks. -This is not the recommended way. +.. warning:: + + Some guides might suggest setting the ``DJANGO_ALLOW_ASYNC_UNSAFE`` environment value to ``True``. + This is **not** the recommended way. The UNSAFE keyword is here for a reason. -For our example, we will use the same file for simplicity, but you can move it to another file, of course. +For our example, we will use the same file for simplicity, but it's fine to move your tasks to another file +(as long as you don't forget to import them !). .. code-block:: python @@ -76,8 +80,8 @@ For our example, we will use the same file for simplicity, but you can move it t def handle(self, *args, **options): app.run() -You can even manually run the `do_things_with_users` function from a view now, -if that's something you would want. Let's add this view in our `views.py` file: +You can even manually run the ``do_things_with_users`` function from a view now, +if that's something you would want. Let's add this view in our ``views.py`` file: .. code-block:: python @@ -121,4 +125,6 @@ if that's something you would want. Let's add this view in our `views.py` file: }, status=HTTP_200_OK) -Note that you will only need to use `sync_to_async` if you use the asynchronous ORM. +.. note :: + You will only need to use ``sync_to_async`` if you use the asynchronous ORM. The usage is well documented in + `Django's documentation `_. diff --git a/docs/cookbook/index.rst b/docs/cookbook/index.rst index b783d543..17adff31 100644 --- a/docs/cookbook/index.rst +++ b/docs/cookbook/index.rst @@ -15,4 +15,5 @@ do more specific things. robust_applications controlling_runtime fastapi - testing \ No newline at end of file + django + testing From e2b2dc0dea6e84a60acb8b4a0e8aa9ecd852beef Mon Sep 17 00:00:00 2001 From: winds0r Date: Sat, 5 Nov 2022 08:04:08 +0900 Subject: [PATCH 4/5] (fix/docs)addressing PR comments --- docs/cookbook/django.rst | 42 +++++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/docs/cookbook/django.rst b/docs/cookbook/django.rst index b7def722..a0e2fbad 100644 --- a/docs/cookbook/django.rst +++ b/docs/cookbook/django.rst @@ -5,7 +5,7 @@ Integrate Django (or Django Rest Framework) This cookbook will use DRF (Django REST Framework) as an example, but the syntax is exaclty the same for Django. -First, let's create a new command for setting up our jobs (we will call it ``inittasks.py``): +First, let's `create a new command `_ for setting up our jobs (we will call it ``inittasks.py``): .. code-block:: python @@ -13,6 +13,8 @@ First, let's create a new command for setting up our jobs (we will call it ``ini from rocketry import Rocketry app = Rocketry(config={"task_execution": "async"}) + # import BaseCommand + from django.core.management.base import BaseCommand # Create some tasks @@ -21,10 +23,10 @@ First, let's create a new command for setting up our jobs (we will call it ``ini ... class Command(BaseCommand): - help = "Setup the periodic tasks runner" + help = "Setup the periodic tasks runner" - def handle(self, *args, **options): - app.run() + def handle(self, *args, **options): + app.run() Next, you can just run in your ``entrypoint`` script (or in your shell) the following command: @@ -63,6 +65,8 @@ For our example, we will use the same file for simplicity, but it's fine to move from rocketry import Rocketry app = Rocketry(config={"task_execution": "async"}) + # import BaseCommand + from django.core.management.base import BaseCommand # this is a synchronous operation that will work in an async context! def do_things_with_users(name): @@ -75,17 +79,18 @@ For our example, we will use the same file for simplicity, but it's fine to move ... class Command(BaseCommand): - help = "Setup the periodic tasks runner" + help = "Setup the periodic tasks runner" - def handle(self, *args, **options): - app.run() + def handle(self, *args, **options): + app.run() You can even manually run the ``do_things_with_users`` function from a view now, if that's something you would want. Let's add this view in our ``views.py`` file: .. code-block:: python - from api.commands import inittasks as tasklist + # this could be any path where the code you want to run is stored + from api.commands import tasks as tasklist import asyncio ... @@ -100,9 +105,10 @@ if that's something you would want. Let's add this view in our ``views.py`` file name = request.GET.get('name') if not name : return Response({ - 'error': 'missing a parameter (expected something like ' - '?name=job_name )' - }, status=HTTP_400_BAD_REQUEST) + 'error': 'missing a parameter (expected something like ?name=job_name )' + }, + status=HTTP_400_BAD_REQUEST, + ) try: @@ -116,13 +122,17 @@ if that's something you would want. Let's add this view in our ``views.py`` file except Exception as err: return Response({ - 'error': 'task failure', - 'logs': f'Failed with: {str(err)}', - }, status=HTTP_500_INTERNAL_SERVER_ERROR) + 'error': 'task failure', + 'logs': f'Failed with: {str(err)}', + }, + status=HTTP_500_INTERNAL_SERVER_ERROR, + ) return Response({ - 'message': 'successfully ran the task', - }, status=HTTP_200_OK) + 'message': 'successfully ran the task', + }, + status=HTTP_200_OK, + ) .. note :: From ec8865ecdf3594de8bf955b40b07c91948f829f8 Mon Sep 17 00:00:00 2001 From: winds0r Date: Sun, 6 Nov 2022 17:34:05 +0900 Subject: [PATCH 5/5] (fix/docs)typo in django cookbook --- docs/cookbook/django.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cookbook/django.rst b/docs/cookbook/django.rst index a0e2fbad..aeb14217 100644 --- a/docs/cookbook/django.rst +++ b/docs/cookbook/django.rst @@ -2,7 +2,7 @@ Integrate Django (or Django Rest Framework) =========================================== -This cookbook will use DRF (Django REST Framework) as an example, but the syntax is exaclty +This cookbook will use DRF (Django REST Framework) as an example, but the syntax is exactly the same for Django. First, let's `create a new command `_ for setting up our jobs (we will call it ``inittasks.py``):