Skip to content

Tips and Tricks for Edgy

This section provides guidance on organizing your code, particularly within an Esmerald application. While the examples are Esmerald-centric, the principles apply to any framework you use with Edgy.

Centralizing Database Connections

Declaring database connections repeatedly throughout your application can lead to redundancy and potential issues with object identity. By centralizing your connections, you ensure consistency and prevent the creation of unnecessary objects.

Global Settings File

A common approach is to store connection details in a global settings file. This is especially convenient with Esmerald, which provides easy access to settings throughout your application.

Example:

"""
Generated by 'esmerald-admin createproject'
"""

from functools import cached_property
from typing import Optional, TYPE_CHECKING

from esmerald.conf.enums import EnvironmentType
from esmerald.conf.global_settings import EsmeraldAPISettings

if TYPE_CHECKING:
    from edgy import Registry, EdgySettings


class AppSettings(EsmeraldAPISettings):
    app_name: str = "My application in production mode."
    environment: Optional[str] = EnvironmentType.PRODUCTION
    secret_key: str = "esmerald-insecure-h35r*b9$+hw-x2hnt5c)vva=!zn$*a7#"  # auto generated

    @cached_property
    def db_connection(self) -> "Registry":
        """
        To make sure the registry and the database connection remains the same
        all the time, always use the cached_property.
        """
        from edgy import Registry

        return Registry(database="postgresql+asyncpg://user:pass@localhost:5432/my_database")

    # optional, in case we want a centralized place
    @cached_property
    def edgy_settings(self) -> "EdgySettings":
        from edgy import EdgySettings

        return EdgySettings(preloads=["myproject.models"])

With this setup, you can access the db_connection from anywhere in your code. In Esmerald:

from esmerald.conf import settings

registry = settings.db_connection

However, merely placing the connection details in a settings file isn't sufficient to ensure object identity. Each time you access settings.db_connection, a new object is created. To address this, we use the lru_cache technique.

The LRU Cache

LRU stands for "Least Recently Used." It's a caching technique that ensures functions with the same arguments return the same cached object. This prevents redundant object creation, which is crucial for maintaining consistent database connections.

Create a utils.py file to apply the lru_cache to your db_connection:

utils.py
from functools import lru_cache


@lru_cache()
def get_db_connection():
    # Encapsulate to delay the import
    from esmerald.conf import settings

    return settings.db_connection

Now, you can import get_db_connection() anywhere in your application and always get the same connection and registry instance.

Important: You cannot place get_db_connection() in the same file as your application entry point. In such cases, use the edgy.monkay.instance sandwich technique.

Excurse: The edgy.monkay.instance Sandwich

If you prefer to consolidate your code within main.py, you can use manual post-loads and initialize connections within get_application. This involves:

  1. Creating the registry.
  2. Assigning the instance to edgy.instance using set_instance() (without app and skipping extensions).
  3. Post-loading models.
  4. Creating the main app.
  5. Assigning the instance to edgy.instance using set_instance() (with app).

Example main.py:

main.py
from importlib import import_module

from esmerald import Esmerald


def build_path():
    """
    Builds the path of the project and project root.
    """
    Path(__file__).resolve().parent.parent
    SITE_ROOT = os.path.dirname(os.path.realpath(__file__))

    if SITE_ROOT not in sys.path:
        sys.path.append(SITE_ROOT)
        sys.path.append(os.path.join(SITE_ROOT, "apps"))


def get_application():
    """
    Encapsulating in methods can be useful for controlling the import order but is optional.
    """
    build_path()
    # import now edgy when the path is set
    import edgy

    registry = edgy.Registry(url=...)
    # extensions shouldn't be applied yet
    edgy.monkay.set_instance(edgy.Instance(registry=registry), apply_extensions=False)
    # load extensions and preloads
    # not evaluate_settings because maybe some preloads weren't resolved
    monkay.evaluate_settings(on_conflict="keep")
    app = Esmerald()
    # now apply the extensions
    edgy.monkay.set_instance(edgy.Instance(registry=registry, app=app))
    return app


application = get_application()

Example myproject/models.py:

myproject/models.py
from datetime import datetime
from enum import Enum

import edgy

registry = edgy.monkay.instance.registry


class ProfileChoice(Enum):
    ADMIN = "ADMIN"
    USER = "USER"


class BaseModel(edgy.Model):
    class Meta:
        abstract = True
        registry = registry


class User(BaseModel):
    """
    Base model for a user
    """

    first_name: str = edgy.CharField(max_length=150)
    last_name: str = edgy.CharField(max_length=150)
    username: str = edgy.CharField(max_length=150, unique=True)
    email: str = edgy.EmailField(max_length=120, unique=True)
    password: str = edgy.CharField(max_length=128)
    last_login: datetime = edgy.DateTimeField(null=True)
    is_active: bool = edgy.BooleanField(default=True)
    is_staff: bool = edgy.BooleanField(default=False)
    is_superuser: bool = edgy.BooleanField(default=False)


class Profile(BaseModel):
    """
    A profile for a given user.
    """

    user: User = edgy.OneToOneField(User, on_delete=edgy.CASCADE)
    profile_type: ProfileChoice = edgy.ChoiceField(ProfileChoice, default=ProfileChoice.USER)

The sandwich method is limited to a single registry, while lru_cache allows for multiple parallel registries.

Practical Example

Let's assemble a practical application with:

  • A User model.
  • Migrations ready.
  • Database connection setup.

Project structure:

.
└── myproject
    ├── __init__.py
    ├── apps
       ├── __init__.py
       └── accounts
           ├── __init__.py
           ├── tests.py
           └── v1
               ├── __init__.py
               ├── schemas.py
               ├── urls.py
               └── views.py
    ├── configs
       ├── __init__.py
       ├── development
          ├── __init__.py
          └── settings.py
       ├── settings.py
       └── testing
           ├── __init__.py
           └── settings.py
    ├── main.py
    ├── serve.py
    ├── utils.py
    ├── tests
       ├── __init__.py
       └── test_app.py
    └── urls.py

Settings

Define database connection properties in settings.py:

my_project/configs/settings.py
"""
Generated by 'esmerald-admin createproject'
"""

from functools import cached_property
from typing import Optional, TYPE_CHECKING

from esmerald.conf.enums import EnvironmentType
from esmerald.conf.global_settings import EsmeraldAPISettings

if TYPE_CHECKING:
    from edgy import Registry, EdgySettings


class AppSettings(EsmeraldAPISettings):
    app_name: str = "My application in production mode."
    environment: Optional[str] = EnvironmentType.PRODUCTION
    secret_key: str = "esmerald-insecure-h35r*b9$+hw-x2hnt5c)vva=!zn$*a7#"  # auto generated

    @cached_property
    def db_connection(self) -> "Registry":
        """
        To make sure the registry and the database connection remains the same
        all the time, always use the cached_property.
        """
        from edgy import Registry

        return Registry(database="postgresql+asyncpg://user:pass@localhost:5432/my_database")

    # optional, in case we want a centralized place
    @cached_property
    def edgy_settings(self) -> "EdgySettings":
        from edgy import EdgySettings

        return EdgySettings(preloads=["myproject.models"])

Utils

Create utils.py with the lru_cache implementation:

myproject/utils.py
from functools import lru_cache


@lru_cache()
def get_db_connection():
    # Encapsulate to delay the import
    from esmerald.conf import settings

    return settings.db_connection

Note: Importing settings directly is not possible here. Wait until build_path is called.

Models

Create models in myproject/apps/accounts/models.py:

myproject/apps/accounts/models.py
from datetime import datetime
from enum import Enum

from my_project.utils import get_db_connection

import edgy

registry = get_db_connection()


class ProfileChoice(Enum):
    ADMIN = "ADMIN"
    USER = "USER"


class BaseModel(edgy.Model):
    class Meta:
        abstract = True
        registry = registry


class User(BaseModel):
    """
    Base model for a user
    """

    first_name: str = edgy.CharField(max_length=150)
    last_name: str = edgy.CharField(max_length=150)
    username: str = edgy.CharField(max_length=150, unique=True)
    email: str = edgy.EmailField(max_length=120, unique=True)
    password: str = edgy.CharField(max_length=128)
    last_login: datetime = edgy.DateTimeField(null=True)
    is_active: bool = edgy.BooleanField(default=True)
    is_staff: bool = edgy.BooleanField(default=False)
    is_superuser: bool = edgy.BooleanField(default=False)


class Profile(BaseModel):
    """
    A profile for a given user.
    """

    user: User = edgy.OneToOneField(User, on_delete=edgy.CASCADE)
    profile_type: ProfileChoice = edgy.ChoiceField(ProfileChoice, default=ProfileChoice.USER)

Use inheritance for cleaner code. Import get_db_connection() to ensure consistent registry usage.

Prepare for Migrations

Configure the application for Edgy migrations in main.py:

myproject/main.py
#!/usr/bin/env python
"""
Generated by 'esmerald-admin createproject'
"""

import os
import sys

from esmerald import Esmerald, Include
from my_project.utils import get_db_connection


def build_path():
    """
    Builds the path of the project and project root.
    """
    SITE_ROOT = os.path.dirname(os.path.realpath(__file__))

    if SITE_ROOT not in sys.path:
        sys.path.append(SITE_ROOT)
        # in case of an application model with apps
        sys.path.append(os.path.join(SITE_ROOT, "apps"))


def get_application():
    """
    Encapsulating in methods can be useful for controlling the import order but is optional.
    """
    # first call build_path
    build_path()
    # because edgy tries to load settings eagerly
    from edgy import monkay, Instance

    registry = get_db_connection()
    # ensure the settings are loaded
    monkay.evaluate_settings(ignore_import_errors=False)

    app = registry.asgi(
        Esmerald(
            routes=[Include(namespace="my_project.urls")],
        )
    )

    monkay.set_instance(Instance(app=app, registry=registry))
    return app


app = get_application()

Hook the Connection

Hook the database connection in main.py using a settings forwarder for centralized configuration management:

myproject/main.py
#!/usr/bin/env python
"""
Generated by 'esmerald-admin createproject'
"""

import os
import sys

from edgy import Instance, monkay
from esmerald import Esmerald, Include


def build_path():
    """
    Builds the path of the project and project root.
    """  #
    SITE_ROOT = os.path.dirname(os.path.realpath(__file__))

    if SITE_ROOT not in sys.path:
        sys.path.append(SITE_ROOT)
        # in case of an application model with apps
        sys.path.append(os.path.join(SITE_ROOT, "apps"))


def get_application():
    """
    Encapsulating in methods can be useful for controlling the import order but is optional.
    """
    # first call build_path
    build_path()
    # import esmerald settings now
    from esmerald.conf import settings

    monkay.settings = lambda: settings.edgy_settings  # rewire the settings
    monkay.evaluate_settings(ignore_import_errors=False)  # import preloads

    # now the project is in the search path and we can import
    from my_project.utils import get_db_connection

    registry = get_db_connection()

    app = registry.asgi(
        Esmerald(
            routes=[Include(namespace="my_project.urls")],
        )
    )

    monkay.set_instance(Instance(registry=registry, app=app))
    return app


app = get_application()

Notes

This example demonstrates how to centralize connection management using lru_cache and settings files. Apply these techniques to your favorite frameworks and adapt them to your specific needs. Edgy is framework-agnostic, providing flexibility and consistency in your database interactions.