Metadata-Version: 2.4
Name: accrete
Version: 0.1.3
Summary: Django Shared Schema Multi Tenant
Author-email: Benedikt Jilek <benedikt.jilek@pm.me>
License: Copyright (c) 2026 Benedikt Jilek
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Internet :: WWW/HTTP
Requires-Python: >=3.10
Requires-Dist: django>=5.2
Provides-Extra: contrib
Requires-Dist: celery>=5.3.4; extra == 'contrib'
Requires-Dist: django-celery-beat; extra == 'contrib'
Requires-Dist: django-template-partials; extra == 'contrib'
Requires-Dist: sqlalchemy; extra == 'contrib'
Provides-Extra: dev
Requires-Dist: build; extra == 'dev'
Requires-Dist: coverage; extra == 'dev'
Requires-Dist: freezegun; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: twine>=4.0.2; extra == 'dev'
Description-Content-Type: text/markdown

# Multi Tenant App for Django
Shared schema multi tenant system for Django.

## Setup
`pip install accrete`  

- Add 'accrete' to INSTALLED_APPS
    
    ```python
    # settings.py
  
    INSTALLED_APPS = [
        ...
        'accrete',
        'django...',
    ]
    ```
- Add accrete.middleware.TenantMiddleware after the auth middleware
    ```python
    # settings.py
  
    MIDDLEWARE = [
        ...
        'django.contrib.auth.middleware.AuthenticationMiddleware',
        'accrete.middleware.TenantMiddleware',
        ...
    ]
    ```
- Run migrations

## Overview
Tenants and their Members are separated by a ForeignKey to accrete.models.Tenant 
and authenticated by request parameters and headers. On Models inheriting from 
accrete.modeles.TenantModel and Forms/ModelForms inheriting from 
accrete.form.(Model)Form, querysets are filtered automatically. 
The authenticated tenant and member are stored in a contextvar and can be 
retrieved and set from anywhere in the code.

## Models
### accrete.models.TenantModel
Abstract Base Model that links to accrete.models.Tenant.  
Sets the objects attribute to accrete.managers.TenantManager.  
Adds safety and integrity checks to the save method.

Inherit form this model to enable tenant separation.  
```python
# models.py

from accrete.models import TenantModel

class MyModel(TenantModel):
    ...
```

### accrete.models.Tenant  
Model representing a tenant.  The active tenant for a request/process is stored
in a contexvar and can be retrieved and set by accrete.tenant.get_tenant and 
accrete.tenant.set_tenant respectively.

### accrete.models.Member  
This model links the user model to the tenant model, allowing users to 
be associated with multiple tenants. The member, like the tenant, is authenticated
by the accrete.middleware.TenantMiddleware and stored in a contextvar.  
The active member can be retived by accrete.tenant.get_mamber and set by
accrete.tenant.set_member. Using set_member also sets the appropriate tenant.

### accret.models.AccessGroup  
Arbitrary authorization groups that can be set on tenant or members.  
These groups are checked on views decorated by accrete.decorator.tenant_reuired 
or subclassed from accrete.mixins.TenantRequiredMixin.

## Manager
##### accrete.managers.TenantManager
Filters QuerySets by the active Tenant.  
Ensures the active tenant is set on the instances on bulk operations.

## Middleware
### tenant.middleware.TenantMiddleware
This Middleware adds the Tenant(request.tenant) and 
Member(request.member) attributes to the request object.  
On responses the X-TENANT-ID header and tenant_id url parameter is added with
the active Tenant-ID as the value.

If the user is a member of multiple tenants, the request is parsed for a 
tenant_id in this order.

- The "tenant_id" URL Parameter in the GET data
- The Header X-TENANT-ID

If no tenant could be assigned the two attributes are set to None.  
Additionally, the user is checked for membership of the found tenant. If the
user has is_staff set to True, the tenant is set regardless of membership.

The Middleware must be added 
to the MIDDLEWARE setting after your authentication Middleware as it needs 
access to request.user.is_authenticated().

## Views
##### tenant.views.TenantRequiredMixin 
Adds tenant and optionally access right checks to the dispatch method.  
This Mixin is meant as a substitute to 
django.contrib.auth.mixins.LoginRequiredMixin as TenantRequiredMixin inherits
LoginRequiredMixin.

##### tenant.decorator.tenant_required  
Substitute for django.contrib.auth.decorators.login_required  
Checks if a tenant is set on the request and redirects to the 
TENANT_NOT_SET_URL specified in the settings.

The decorator itself is wrapped by login_required and can pass the arguments
redirect_field_name and login_url to login_required.

## Forms
##### accrete.forms.Form
Form class that filters the queryset of every field with a queryset attribute.

##### accrete.forms.ModelForm
Same behavior as tenant.forms.Form. Additionally, sets the tenant 
on the instance on save if needed.

## Fields
##### accrete.fields.TranslatedCharField
Extends JsonField to store strings in different languages with the language code as the key.  
By default, the language specified in settings.LANGUAGE_CODE is used to store values.
```python
from django.db import models
from django.utils import translation
from accrete.utils.models import translated_db_value


class MyModel(models.Model):
    name = TranslatedCharField(...)

    
# Create instance with the language set in settings.LANGUAGE_CODE
instance = MyModel(name='Name in default language')
instance.save()

# Switch to german
translation.activate('de-de')
instance.name = 'Name auf Deutsch'
instance.save()

# Switch back to english
translation.activate('en-us')
instance.refresh_from_db()
print(instance.name)
>>> 'Name in default language'

# Utility to get the saved json as a dict
print(translated_db_value(instance, 'name'))
>>> {'en-us': 'Name in default language', 'de-de': 'Name auf Deutsch'}

# Create an instance with multiple translations at once
translation.activate('de-de')
instance = MyModel(name={'en-us': 'Name in default language', 'de-de': 'Name auf Deutsch'})
instance.save()
print(instance.name)
>>> 'Name auf Deutsch'

```

## Channels
##### accrete.channels.TenantMiddleware
Support for django-channels. Sets the tenant and member contextvar.  
Must be inside auth middleware.

```python
# asgi.py

application = ProtocolTypeRouter({
    "http": django_asgi_app,
    "websocket": AllowedHostsOriginValidator(
        AuthMiddlewareStack(
            TenantMiddleware(
                URLRouter([
                    path('path/to/consumer/', Consumer.as_asgi()),
                ])
            )
        )
    ),
})

```

## Consumer
##### accrete.consumer.WebsocketTenantConsumer
Checks on connect if a tenant is set.

##### accrete.consumer.JsonWebsocketTenantConsumer
Checks on connect if a tenant is set.

## Settings

- ACCRETE_TENANT_NOT_SET_URL
Redirect to this URL when no tenant could be set for an authenticated user.


- ACCRETE_GROUP_NOT_SET_URL  
Redirect to this URL when a tenant or member tries to access a URL 
without having the needed access rights.

## Tenant Utils
### Tenant
##### accrete.tenant.set_tenant(tenant: accrete.models.Tenant)
Stores the tenant in contextvar.

##### accrete.tenant.get_tenant
Retrieves the tenant from contextvar.

##### accrete.tenant.set_member(member: accrete.models.Member)
Stores the member and associated tenant contextvar.

##### accrete.tenant.set_member(member: accrete.models.Member)
Retrieves the member from contextvar.

##### accrete.tenant.unscoped()
Context Manager to temporally disable tenant isolation.

##### accrete.tenant.unscope()
Decorator to disable tenant isolation.

##### accrete.tenant.per_tenant(include: Q = None, exclude: Q = None)
Decorator to run the decorated function per tenant.
Tenant to run on can be filtered by the include or exclude arguments.

## Contrib
Additional Apps

| App               | Description                                                    |  
|-------------------|----------------------------------------------------------------|
| country           | Countries with translatable names                              |
| log               | Global auditlog, runs at post_save signal                      |
| sequence          | Configurable sequences with support for subsequences per year. |
| system_mail       | Celery mail queue                                              |
| ui                | User Interface based on HTMX, Alpine.js and Bulma              |
| user              | Custom User model                                              |
| user_registration | User registration using system_mail to send confirmation mails |
