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
:
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:
- Creating the registry.
- Assigning the instance to
edgy.instance
usingset_instance()
(without app and skipping extensions). - Post-loading models.
- Creating the main app.
- Assigning the instance to
edgy.instance
usingset_instance()
(with app).
Example 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
:
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
:
"""
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:
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
:
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
:
#!/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:
#!/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.