Skip to content

Custom Directives

Having built-in directives from Ravyn is great as it gives you a lot of niceties for your project but having custom directives is what really powers up your application and takes it to another level.

Important

Before reading this section, you should get familiar with the ways Ravyn handles the discovery of the applications.

The following examples and explanations will be using the --app and environment variables approach but the auto discovery is equally valid and works in the same way.

What is a custom directive?

Before jumping into that, let us go back to the roots of python.

Python was and still is heavily used as a scripting language. The scripts are isolated pieces of code and logic that can run on every machine that has python installed and execute without too much trouble or hurdle.

Quite simple, right?

So, what does this have to do with directives? Well, directives follow the same principle but applied to your own project. What if you could create your own structured scripts inside your project directly? What if you could build dependent or independent pieces of logic that could be run using your own Ravyn application resources?

This is what a directive is.

Tip

If you are familiar with Django management commands, Ravyn directives follow the same principle. There is an excelent article about those if you want to get familiar with.

Examples

Imagine you need to deploy a database that will contain all the information about specific user accesses and will manage roles of your application.

Now, once that database is deployed with your application, usually would would need somehow to connect to your production server and manually setup a user or run a specific script or command to create that same super user. This can be time consuming and prone to errors, right?

You can use a directive to do that same job for you.

Or what if you need to create specific operations to run in the background by some ops that does not require APIs, for example, update the role of a user? Directives solve that problem as well.

There is a world of possibilities of what you can do with directives.

Directive

This is the main object class for every single custom directive you want to implement. This is a special object with some defaults that you can use.

Directives were inspired by the management commands of Django with extra flavours and therefore the syntax is very similar.

Parameters

  • --directive - The directive name (the file where the Directive was created). Check list all directives for more details in obtaining the names.

How to run

The syntax is very simple for a custom directive:

With the --app parameter

$ ravyn --app <LOCATION> run --directive <DIRECTIVE-NAME> <OPTIONS>

Example:

ravyn --app myproject.main:app run --directive mydirective --name ravyn

With the RAVYN_DEFAULT_APP environment variable set

$ export RAVYN_DEFAULT_APP=myproject.main:app
$ ravyn run --directive <DIRECTIVE-NAME> <OPTIONS>

Example:

$ export RAVYN_DEFAULT_APP=myproject.main:app
$ ravyn run --directive mydirective --name ravyn

The run --directive is always expecting the name of the file of your directive.

For example, you created a createsuperuser.py file with your Directive logic. The --directive parameter will be run --directive createsuperuser.

Example:

$ export RAVYN_DEFAULT_APP=myproject.main:app
$ ravyn run --directive createsuperuser --email example@ravyn.dev

How to create a directive

To create a directive you must inherit from the BaseDiretive class and must call Directive to your object.

from ravyn.core.directives import BaseDirective

Create the Directive class

import argparse
from typing import Any, Type

from ravyn.core.directives import BaseDirective


class Directive(BaseDirective):
    def add_arguments(self, parser: Type["argparse.ArgumentParser"]) -> Any:
        # Add argments
        ...

Every single custom directive created should be called Directive and must inherit from the BaseDiretive class.

Internally ravyn looks for a Directive object and verifies if is a subclass of BaseDirective. If one of this conditions fails, it will raise a DirectiveError.

Where should directives be placed at?

All the custom directives created must be inside a directives/operations package in order to be discovered.

The place for the directives/operations can be anywhere in your application and you can have more than one as well.

Example:

.
├── Taskfile.yaml
└── myproject
    ├── __init__.py
    ├── apps
       ├── accounts
          ├── directives
             ├── __init__.py
             └── operations
                 ├── createsuperuser.py
                 └── __init__.py
       ├── payroll
          ├── directives
             ├── __init__.py
             └── operations
                 ├── run_payroll.py
                 └── __init__.py
       ├── products
          ├── directives
             ├── __init__.py
             └── operations
                 ├── createproduct.py
                 └── __init__.py
    ├── configs
       ├── __init__.py
       ├── development
          ├── __init__.py
          └── settings.py
       ├── settings.py
       └── testing
           ├── __init__.py
           └── settings.py
    ├── directives
       ├── __init__.py
       └── operations
           ├── db_shell.py
           └── __init__.py
    ├── main.py
    ├── serve.py
    ├── tests
       ├── __init__.py
       └── test_app.py
    └── urls.py

As you can see from the previous example, we have four directives:

  • createsuperuser - Inside accounts/directives/operations.
  • run_payroll - Inside payroll/directives/operations.
  • createproduct - Inside products/directives/operations.
  • db_shell - Inside ./directives/operations.

All of them, no matter where you put the directive, are inside a directives/operations where ravyn always looks at.

Directive functions

handle()

The Diretive logic is implemented inside a handle function that can be either sync or async.

When calling a Directive, ravyn will execute the handle() and run the all the logic.

import argparse
from typing import Any, Type

from ravyn.core.directives import BaseDirective
from ravyn.core.terminal import Print

printer = Print()


class Directive(BaseDirective):
    def add_arguments(self, parser: Type["argparse.ArgumentParser"]) -> Any:
        # Add argments
        ...

    def handle(self, *args: Any, **options: Any) -> Any:
        # Runs the handle logic in sync mode
        printer.write_success("Sync mode handle run with success!")
import argparse
from typing import Any, Type

from ravyn.core.directives import BaseDirective
from ravyn.core.terminal import Print

printer = Print()


class Directive(BaseDirective):
    def add_arguments(self, parser: Type["argparse.ArgumentParser"]) -> Any:
        # Add argments
        ...

    async def handle(self, *args: Any, **options: Any) -> Any:
        # Runs the handle logic in async mode
        printer.write_success("Async mode handle run with success!")

As you can see, Ravyn Directives also allow async and sync type of functions. This can be particularly useful for when you need to run specific tasks in async mode, for example.

add_arguments()

This is the place where you add any argument needed to run your custom directive. The arguments are argparse related arguments so the syntax should be familiar.

import argparse
from typing import Any, Type

from ravyn.core.directives import BaseDirective
from ravyn.core.terminal import Print

printer = Print()


class Directive(BaseDirective):
    def add_arguments(self, parser: Type["argparse.ArgumentParser"]) -> Any:
        """Arguments needed to create a user"""
        parser.add_argument("--first-name", dest="first_name", type=str, required=True)
        parser.add_argument("--last-name", dest="last_name", type=str, required=True)
        parser.add_argument("--username", dest="username", type=str, required=True)
        parser.add_argument("--email", dest="email", type=str, required=True)
        parser.add_argument("--password", dest="password", type=str, required=True)

    async def handle(self, *args: Any, **options: Any) -> Any:
        # Runs the handle logic in async mode
        ...

As you can see, the Directive has five parameters and all of them required.

ravyn --app teste.main:app run --directive mydirective --first-name Ravyn --last-name Framework --email example@ravyn.dev --username ravyn --password ravyn

Help

There are two helps in place for the directives. The one you run the ravyn executor (run) and the one for the directive.

--help

This command is only used for the executor help, for example:

$ ravyn run --help

-h

This flag is used to access the directive help and not the run.

$ ravyn run --directive mydirective -h

Notes

The only way to see the help of a directive is via -h.

If --help is used, it will only show the help of the run and not the directive itself.

Order of priority

This is very important to understand.

What happens if we have two custom directives with the same name?

Let us use the following structure as example:

.
├── Taskfile.yaml
└── myproject
    ├── __init__.py
    ├── apps
       ├── accounts
          ├── directives
             ├── __init__.py
             └── operations
                 ├── createsuperuser.py
                 └── __init__.py
          ├── __init__.py
          ├── models.py
          ├── tests.py
          └── v1
              ├── __init__.py
              ├── schemas.py
              ├── urls.py
              └── controllers.py
    ├── configs
       ├── __init__.py
       ├── development
          ├── __init__.py
          └── settings.py
       ├── settings.py
       └── testing
           ├── __init__.py
           └── settings.py
    ├── directives
       ├── __init__.py
       └── operations
           ├── createsuperuser.py
           └── __init__.py
    ├── main.py
    ├── serve.py
    ├── tests
       ├── __init__.py
       └── test_app.py
    └── urls.py

This example is simulating a structure of an ravyn project with two custom directives with the same name.

The first directive is inside ./directives/operations/ and the second inside ./apps/accounts/directives/operations.

Ravyn directives work on a First Found First Executed principle and that means if you have two custom directives with the same name, ravyn will execute the first found directive with that given name.

In other words, if you want to execute the createsuperuser from the accounts, the first found directive inside ./directives/operations/ shall have a different name or else it will execute it instead of the intended from accounts.

Execution

Ravyn directives use the same events as the one passed in the application.

For example, if you want to execute database operations and the database connections should be established before hand, you can do in two ways:

  • Use Lifespan events and the directives will use them.
  • Establish the connections (open and close) inside the Directive directly.

The pratical example uses the lifespan events to execute the operations. This way you only need one place to manage the needed application events.

A practical example

Let us run an example of a custom directive for your application. Since we keep mentioning the createsuperuser often, let us then create that same directive and apply to our Ravyn application.

For this example we will be using Edgy since it is from the same author and will allow us to do a complete end-to-end directive using the async approach.

This example is very simple in its own design.

For production you should have your models inside a models dedicated place and your registry and database settings somewhere in your settings where you can access it anywhere in your code via ravyn settings, for example.

P.S.: For the registry and database strategy with edgy, it is good to have a read the tips and tricks with edgy.

The design is up to you.

What we will be creating:

  • myproject/main/main.py - The entry-point for our Ravyn application
  • createsuperuser - Our directive.

In the end we simply run the directive.

We will be also using the edgy support from Ravyn models as this will make the example simpler.

The application entrypoint

myproject/main.py
import edgy
from edgy import Database, Registry

from ravyn import Ravyn
from ravyn.contrib.auth.edgy.base_user import AbstractUser

database = Database("postgres://postgres:password@localhost:5432/my_db")
registry = Registry(database=database)


class User(AbstractUser):
    date_of_birth = edgy.DateField(null=True)

    class Meta:
        registry = registry


def get_application():
    """
    This is optional. The function is only used for organisation purposes.
    """

    app = Ravyn(
        routes=[],
        on_startup=[database.__anter__],
        on_shutdown=[database.__aexit__],
    )

    return app


app = get_application()

The connection string should be replaced with whatever is your detail.

The createsuperuser

Now it is time to create the directive createsuperuser. As mentioned above, the directive shall be inside a directives/operations package.

myproject/directives/operations/createsuperuser.py
import argparse
import random
import string
from typing import Any, Type

from asyncpg.exceptions import UniqueViolationError

from ravyn.core.directives import BaseDirective
from ravyn.core.terminal import Print

from ..main import User

printer = Print()


class Directive(BaseDirective):
    help: str = "Creates a superuser"

    def add_arguments(self, parser: Type["argparse.ArgumentParser"]) -> Any:
        parser.add_argument("--first-name", dest="first_name", type=str, required=True)
        parser.add_argument("--last-name", dest="last_name", type=str, required=True)
        parser.add_argument("--username", dest="username", type=str, required=True)
        parser.add_argument("--email", dest="email", type=str, required=True)
        parser.add_argument("--password", dest="password", type=str, required=True)

    def get_random_string(self, length=10):
        letters = string.ascii_lowercase
        result_str = "".join(random.choice(letters) for i in range(length))
        return result_str

    async def handle(self, *args: Any, **options: Any) -> Any:
        """
        Generates a superuser
        """
        first_name = options["first_name"]
        last_name = options["last_name"]
        username = options["username"]
        email = options["email"]
        password = options["password"]

        try:
            user = await User.query.create_superuser(
                first_name=first_name,
                last_name=last_name,
                username=username,
                email=email,
                password=password,
            )
        except UniqueViolationError:
            printer.write_error(f"User with email {email} already exists.")
            return

        printer.write_success(f"Superuser {user.email} created successfully.")

And this should be it. We now have a createsuperuser and an application and now we can run in the command line:

Using the auto discover

$ ravyn run --directive createsuperuser --first-name Ravyn --last-name Framework --email example@ravyn.dev --username ravyn --password ravyn

Using the --app or RAVYN_DEFAULT_APP

$ ravyn --app myproject.main:app run --directive createsuperuser --first-name Ravyn --last-name Framework --email example@ravyn.dev --username ravyn --password ravyn

Or

$ export RAVYN_DEFAULT_APP=myproject.main:app
$ ravyn run --directive createsuperuser --first-name Ravyn --last-name Framework --email example@ravyn.dev --username ravyn --password ravyn

After the command is executed, you should be able to see the superuser created in your database.