Tips and tricks¶
This part is dedicated to some code organisation within your application.
The examples are more focused on the Esmerald as the author is the same but again, you can do the same in your favourite framework.
Placing your connection in a centralised place¶
This is probably what you would like to do in your application since you don't want to declare over and over again the same variables.
The main reason for that is the fact that every time you declare a registry or a
database
, in fact you are generating a new object and this is not great if you need to access
the models used with the main registry, right?
Place the connection details inside a global settings file¶
This is probably the easiest way to place the connection details and particulary for Esmerald since it comes with a simple and easy way of accesing the settings anywhere in the code.
Something simple like this:
"""
Generated by 'esmerald-admin createproject'
"""
from functools import cached_property
from typing import Optional, Tuple
from esmerald.conf.enums import EnvironmentType
from esmerald.conf.global_settings import EsmeraldAPISettings
from saffier import Database, Registry
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) -> Tuple[Database, Registry]:
"""
To make sure the registry and the database connection remains the same
all the time, always use the cached_property.
"""
database = Database("postgresql+asyncpg://user:pass@localhost:5432/my_database")
return database, Registry(database=database)
As you can see, now you have the db_connection
in one place and easy to access from anywhere in
your code. In the case of Esmerald:
from esmerald.conf import settings
database, registry = settings.db_connection
But is this enough?. No.
As mentioned before, when assigning or creating a variable, python itself generates a new object
with a different id
which can differ from each time you need to import the settings into the
needed places.
We won't talk about this pythonic tricks as there are plenty of documentation on the web and better suited for that same purpose.
How do we solve this issue? Enters lru_cache.
The LRU cache¶
LRU extends for least recently used.
A very common technique that aims to help caching certain pieces of functionality within your codebase and making sure you do not generate extra objects and this is exactly what we need.
Use the example above, let us now create a new file called utils.py
where we will be applying
the lru_cache
technique for our db_connection
.
from functools import lru_cache
from esmerald.conf import settings
@lru_cache()
def get_db_connection():
database, registry = settings.db_connection
return database, registry
This will make sure that from now on you will always use the same connection and registry within
your appliction by importing the get_db_connection()
anywhere is needed.
Pratical example¶
Let us now assemble everything and generate an application that will have:
- User model
- Ready to generate migrations
- Starts the database connections
For this example we will have the following structure (we won't be use using all of the files). We won't be creating views as this is not the purpose of the example.
.
└── 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
This structure is generated by using the Esmerald directives
The settings¶
As mentioned before we will have a settings file with database connection properties assembled.
"""
Generated by 'esmerald-admin createproject'
"""
from functools import cached_property
from typing import Optional, Tuple
from esmerald.conf.enums import EnvironmentType
from esmerald.conf.global_settings import EsmeraldAPISettings
from saffier import Database, Registry
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) -> Tuple[Database, Registry]:
"""
To make sure the registry and the database connection remains the same
all the time, always use the cached_property.
"""
database = Database("postgresql+asyncpg://user:pass@localhost:5432/my_database")
return database, Registry(database=database)
The utils¶
Now we create the utils.py
where we appy the LRU technique.
from functools import lru_cache
from esmerald.conf import settings
@lru_cache()
def get_db_connection():
database, registry = settings.db_connection
return database, registry
The models¶
We can now start creating our models and making sure we keep them always in the same registry
from enum import Enum
from my_project.utils import get_db_connection
import saffier
_, registry = get_db_connection()
class ProfileChoice(Enum):
ADMIN = "ADMIN"
USER = "USER"
class BaseModel(saffier.Model):
class Meta:
abstract = True
registry = registry
class User(BaseModel):
"""
Base model for a user
"""
first_name = saffier.CharField(max_length=150)
last_name = saffier.CharField(max_length=150)
username = saffier.CharField(max_length=150, unique=True)
email = saffier.EmailField(max_length=120, unique=True)
password = saffier.CharField(max_length=128)
last_login = saffier.DateTimeField(null=True)
is_active = saffier.BooleanField(default=True)
is_staff = saffier.BooleanField(default=False)
is_superuser = saffier.BooleanField(default=False)
class Profile(BaseModel):
"""
A profile for a given user.
"""
user = saffier.OneToOneField(User, on_delete=saffier.CASCADE)
profile_type = saffier.ChoiceField(ProfileChoice, default=ProfileChoice.USER)
Here applied the inheritance to make it clean and more readable in case we want even more models.
As you could also notice, we are importing the get_db_connection()
previously created. This is
now what we will be using everywhere.
Prepare the application to allow migrations¶
Now it is time to tell the application that your models and migrations are managed by Saffier. More information on migrations where explains how to use it.
#!/usr/bin/env python
"""
Generated by 'esmerald-admin createproject'
"""
import os
import sys
from pathlib import Path
from esmerald import Esmerald, Include
from my_project.utils import get_db_connection
from saffier.cli import Migrate
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():
"""
This is optional. The function is only used for organisation purposes.
"""
build_path()
database, registry = get_db_connection()
app = Esmerald(
routes=[Include(namespace="my_project.urls")],
)
Migrate(app=app, registry=registry)
return app
app = get_application()
This will make sure that your application migrations are now managed by Saffier.
Hook the connection¶
As a final step we now need to make sure we hook the connection in our application.
#!/usr/bin/env python
"""
Generated by 'esmerald-admin createproject'
"""
import os
import sys
from pathlib import Path
from esmerald import Esmerald, Include
from my_project.utils import get_db_connection
from saffier.cli import Migrate
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():
"""
This is optional. The function is only used for organisation purposes.
"""
build_path()
database, registry = get_db_connection()
app = Esmerald(
routes=[Include(namespace="my_project.urls")],
on_startup=[database.connect],
on_shutdown=[database.disconnect],
)
Migrate(app=app, registry=registry)
return app
app = get_application()
And this is it.
Notes¶
The above example shows how you could take leverage of a centralised place to manage your connections and then use it across your application keeping your code always clean not redundant and beautiful.
This example is applied to any of your favourite frameworks and you can use as many and different techniques as the ones you see fit for your own purposes.
Saffier is framework agnostic.