🚀 ADjango
📊 Coverage 70%
Sometimes I use this in different projects, so I decided to put it on pypi
ADjango is a comprehensive library that enhances Django development with Django REST Framework (DRF) and Celery
integration. It provides essential tools including
asynchronous services, serializers, decorators, exceptions and more utilities for async
programming, Celery task scheduling, transaction management, and much more to streamline your Django DRF Celery
development workflow.
Installation 🛠️
pip install adjango
Settings ⚙️
-
Add the application to the project
INSTALLED_APPS = [
'adjango',
]
-
In settings.py set the params
LOGIN_URL = '/login/'
ADJANGO_BACKENDS_APPS = BASE_DIR / 'apps'
ADJANGO_FRONTEND_APPS = BASE_DIR.parent / 'frontend' / 'src' / 'apps'
ADJANGO_APPS_PREPATH = 'apps.'
ADJANGO_UNCAUGHT_EXCEPTION_HANDLING_FUNCTION = ...
ADJANGO_CONTROLLERS_LOGGER_NAME = 'global'
ADJANGO_CONTROLLERS_LOGGING = True
ADJANGO_EMAIL_LOGGER_NAME = 'email'
MIDDLEWARE = [
...
'adjango.middleware.IPAddressMiddleware',
...
]
Overview
Most functions, if available in asynchronous form, are also available in synchronous form.
Models & Services 🛎️
A simple example and everything is immediately clear...
from django.contrib.auth.models import AbstractUser
from django.db.models import CASCADE, CharField, ForeignKey, ManyToManyField
from adjango.models import Model
from adjango.models.polymorphic import PolymorphicModel
from adjango.services.base import BaseService
from adjango.utils.funcs import aadd, aall, afilter, aset
...
...
...
if TYPE_CHECKING:
from apps.core.models import User
class UserService(BaseService):
def __init__(self, obj: 'User') -> None:
self.user = obj
def get_full_name(self) -> str:
return f"{self.user.first_name} {self.user.last_name}"
class User(AbstractUser):
...
@property
def service(self) -> UserService:
return UserService(self)
full_name = user.service.get_full_name()
...
...
...
class Product(PolymorphicModel):
name = CharField(max_length=100)
class Order(Model):
user = ForeignKey(User, CASCADE)
products = ManyToManyField(Product)
products = await afilter(Product.objects, name='name')
order = await BaseService.agetorn(Order.objects, id=69)
if not order: raise
await aset(order.products, products)
await aset(
order.products,
Product.objects.filter(name='name')
)
await aadd(order.products, products[0])
order: Order = await Order.objects.aget(id=69)
order.user = await order.arelated('user')
products = await aall(order.products)
orders = await aall(Order.objects.prefetch_related('products'))
for o in orders:
for p in o.products.all():
print(p.id)
Utils 🔧
aall, afilter, arelated, and so on are available as individual functions
from adjango.utils.funcs import (
aall, afilter, aset, aadd, arelated
)
ATextChoices and AIntegerChoices extend Django TextChoices / IntegerChoices
with helpers:
get_label(value) -> label or None
has_value(value) -> bool
as_dict() -> {value: label}
values and labels are available as standard Django choices attributes.
from adjango.models.choices import AIntegerChoices, ATextChoices
class OrderStatus(ATextChoices):
NEW = 'new', 'New'
PAID = 'paid', 'Paid'
class Priority(AIntegerChoices):
LOW = 1, 'Low'
HIGH = 2, 'High'
OrderStatus.get_label('new')
OrderStatus.get_label(OrderStatus.PAID)
OrderStatus.get_label('unknown')
OrderStatus.has_value('new')
Priority.as_dict()
Priority.values
Priority.labels
Mixins 🎨
from adjango.models.mixins import (
CreatedAtMixin, CreatedAtIndexedMixin, CreatedAtEditableMixin,
UpdatedAtMixin, UpdatedAtIndexedMixin,
CreatedUpdatedAtMixin, CreatedUpdatedAtIndexedMixin
)
class EventProfile(CreatedUpdatedAtIndexedMixin):
event = ForeignKey('events.Event', CASCADE, 'members', verbose_name=_('Event'))
@property
def service(self) -> EventProfileService:
return EventProfileService(self)
Decorators 🎀
-
aforce_data
The aforce_data decorator combines data from the GET, POST and JSON body
request in request.data. This makes it easy to access all request data in one place.
-
aatomic
An asynchronous decorator that wraps function into a transactional context using AsyncAtomicContextManager. If an
exception occurs, all database changes are rolled back.
-
acontroller/controller
Decorators that provide automatic logging and exception handling for views. The acontroller is for async
views, controller is for sync views. They do NOT wrap functions in transactions (use @aatomic for that).
from adjango.adecorators import acontroller
from adjango.decorators import controller
@acontroller(name='My View', logger='custom_logger', log_name=True, log_time=True)
async def my_view(request):
pass
@acontroller('One More View')
async def my_view_one_more(request):
pass
@controller(name='Sync View', auth_required=True, log_time=True)
def my_sync_view(request):
pass
-
These decorators automatically catch uncaught exceptions and log them if the logger is configured
via ADJANGO_CONTROLLERS_LOGGER_NAME and ADJANGO_CONTROLLERS_LOGGING.
-
The controller decorator also supports authentication checking with auth_required parameter.
-
You can also implement the interface:
class IHandlerControllerException(ABC):
@staticmethod
@abstractmethod
def handle(fn_name: str, request: WSGIRequest | ASGIRequest, e: Exception, *args, **kwargs) -> None:
"""
An example of an exception handling function.
:param fn_name: The name of the function where the exception occurred.
:param request: The request object (WSGIRequest or ASGIRequest).
:param e: The exception to be handled.
:param args: Positional arguments passed to the function.
:param kwargs: Named arguments passed to the function.
:return: None
"""
pass
and use handle to get an uncaught exception:
from adjango.handlers import HCE
ADJANGO_UNCAUGHT_EXCEPTION_HANDLING_FUNCTION = HCE.handle
Exceptions 🚨
ADjango provides convenient classes for generating API exceptions with proper HTTP statuses and structured error
messages.
from adjango.exceptions.base import (
ApiExceptionGenerator,
ModelApiExceptionGenerator,
ModelApiExceptionBaseVariant as MAEBV
)
raise ApiExceptionGenerator('Special error', 500)
raise ApiExceptionGenerator('Special error', 500, 'special_error')
raise ApiExceptionGenerator(
'Wrong data',
400,
extra={'field': 'email'}
)
from apps.commerce.models import Order
raise ModelApiExceptionGenerator(Order, MAEBV.DoesNotExist)
raise ModelApiExceptionGenerator(
Order,
MAEBV.AlreadyExists,
code="order_exists",
extra={"id": 123}
)
Serializers 🔧
ADjango extends Django REST Framework serializers to support asynchronous
operations, making it easier to handle data in async views.
Support methods like adata, avalid_data, ais_valid, and asave.
from adjango.aserializers import (
AModelSerializer, ASerializer, AListSerializer
)
from adjango.serializers import dynamic_serializer
from adjango.services.base import BaseService
from adjango.utils.funcs import aall
from django.db.models import QuerySet
...
class ConsultationPublicSerializer(AModelSerializer):
clients = UserPublicSerializer(many=True, read_only=True)
psychologists = UserPsyPublicSerializer(many=True, read_only=True)
config = ConsultationConfigSerializer(read_only=True)
class Meta:
model = Consultation
fields = '__all__'
ConsultationSerializerTier1 = dynamic_serializer(
ConsultationPublicSerializer, ('id', 'date',)
)
ConsultationSerializerTier2 = dynamic_serializer(
ConsultationPublicSerializer, (
'id', 'date', 'psychologists', 'clients', 'config'
), {
'psychologists': UserPublicSerializer(many=True),
}
)
@acontroller('Completed Consultations')
@api_view(('GET',))
@permission_classes((IsAuthenticated,))
async def consultations_completed(request):
page = int(request.query_params.get('page', 1))
page_size = int(request.query_params.get('page_size', 10))
return Response({
'results': await ConsultationSerializerTier2(
await aall(
request.user.service.completed_consultations[
(page - 1) * page_size:page * page_size
]
),
many=True,
context={'request': request}
).adata
}, status=200)
...
class UserService(BaseService):
...
@property
def completed_consultations(self) -> QuerySet['Consultation']:
"""
Returns an optimized QuerySet of all completed consultations of the user
(both psychologist and client).
"""
from apps.psychology.models import Consultation
now_ = now()
return Consultation.objects.defer(
'communication_type',
'language',
'reserved_by',
'notifies',
'cancel_initiator',
'original_consultation',
'consultations_feedbacks',
).select_related(
'config',
'conference',
).prefetch_related(
'clients',
'psychologists',
).filter(
Q(
Q(clients=self.user) | Q(psychologists=self.user),
status=Consultation.Status.PAID,
date__isnull=False,
date__lt=now_,
consultations_feedbacks__user=self.user,
) |
Q(
Q(clients=self) | Q(psychologists=self.user),
status=Consultation.Status.CANCELLED,
date__isnull=False,
)
).distinct().order_by('-updated_at')
...
Management
copy_project
Documentation in the py module itself - copy_project
ADjango ships with extra management commands to speed up project scaffolding.
-
astartproject — clones the adjango-template
into the given directory and strips its Git history.
django-admin astartproject myproject
-
astartup — creates an app skeleton inside apps/ and registers it in
INSTALLED_APPS.
python manage.py astartup blog
After running the command you will have the following structure:
apps/
blog/
controllers/base.py
models/base.py
services/base.py
serializers/base.py
tests/base.py
-
newentities — generates empty exception, model, service, serializer and
test stubs for the specified models in the target app.
python manage.py newentities order apps.commerce Order,Product,Price
Or create a single model:
python manage.py newentities order apps.commerce Order
Celery 🔥
ADjango provides convenient tools for working with Celery: management commands, decorators, and task scheduler.
For Celery configuration in Django, refer to
the official Celery documentation.
Management Commands
-
celeryworker — starts Celery Worker with default settings
python manage.py celeryworker
python manage.py celeryworker --pool=solo --loglevel=info -E
python manage.py celeryworker --concurrency=4 --queues=high_priority,default
-
celerybeat — starts Celery Beat scheduler for periodic tasks
python manage.py celerybeat
python manage.py celerybeat --loglevel=debug
-
celerypurge — clears Celery queues from unfinished tasks
python manage.py celerypurge
python manage.py celerypurge --queue=high
@task Decorator
The @task decorator automatically logs Celery task execution, including errors:
from celery import shared_task
from adjango.decorators import task
@shared_task
@task(logger="global")
def my_background_task(param1: str, param2: int) -> bool:
"""
Task with automatic execution logging.
"""
return True
What the decorator provides:
- ✅ Automatic logging of task start and completion
- ✅ Logging of task parameters
- ✅ Detailed error logging with stack trace
- ✅ Flexible logger configuration for different tasks
Tasker - Task Scheduler
The Tasker class provides convenient methods for scheduling and managing Celery tasks:
from adjango.utils.celery.tasker import Tasker
task_id = Tasker.put(task=my_task, param1='value')
task_id = Tasker.put(task=my_task, countdown=60, param1='value')
from datetime import datetime
task_id = Tasker.put(
task=my_task,
eta=datetime(2024, 12, 31, 23, 59),
param1='value'
)
Tasker.cancel_task(task_id)
Tasker.beat(
task=my_task,
name='one_time_task',
schedule_time=datetime(2024, 10, 10, 14, 30),
param1='value'
)
Tasker.beat(
task=my_task,
name='hourly_cleanup',
interval=3600,
param1='value'
)
Tasker.beat(
task=my_task,
name='daily_report',
crontab={'hour': 7, 'minute': 30},
param1='value'
)
await Tasker.abeat(
task=my_task,
name='async_task',
interval=1800,
param1='value'
)
Email Sending via Celery
ADjango includes a ready-to-use task for sending emails with templates:
from adjango.tasks import send_emails_task
from adjango.utils.mail import send_emails
send_emails(
subject='Welcome!',
emails=('user@example.com',),
template='emails/welcome.html',
context={'user': 'John Doe'}
)
send_emails_task.delay(
subject='Hello!',
emails=('user@example.com',),
template='emails/hello.html',
context={'message': 'Welcome to our service!'}
)
Tasker.put(
task=send_emails_task,
subject='Reminder',
emails=('user@example.com',),
template='emails/reminder.html',
context={'deadline': '2024-12-31'},
countdown=3600
)
Other
-
AsyncAtomicContextManager🧘
An asynchronous context manager for working with transactions, which ensures the atomicity of operations.
from adjango.utils.base import AsyncAtomicContextManager
async def some_function():
async with AsyncAtomicContextManager():
...