Tenant context helpers.
This module provides a thread-safe context for tracking the "current"
tenant, database alias and cache alias using Python's contextvars.
It exposes the class:TenantContext helper which maintains per-context
stacks for tenant, database alias and cache alias values and offers
convenience context managers to temporarily switch to a tenant, the
master database, a public schema, or an arbitrary schema.
These utilities are intended for use by code that must run under a
specific tenant/environment (for example during request handling,
management commands or background tasks).
Example
with TenantContext.use_tenant(my_tenant):
# code here runs with DB/cache backends activated for my_tenant
...
TenantContext
A context manager for tenant, database and cache selection.
TenantContext stores three independent per-context stacks backed by
:class:contextvars.ContextVar:
- tenant stack: the active tenant objects
- db alias stack: the active Django DB alias to use
- cache alias stack: the active cache alias to use
The stacks behave like simple lists: callers push a value before
entering a temporary context and pop it when leaving. Several
convenience context managers are provided to activate/deactivate
tenant-related backends when switching contexts.
Source code in django_omnitenant/tenant_context.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319 | class TenantContext:
"""A context manager for tenant, database and cache selection.
TenantContext stores three independent per-context stacks backed by
:class:`contextvars.ContextVar`:
- tenant stack: the active tenant objects
- db alias stack: the active Django DB alias to use
- cache alias stack: the active cache alias to use
The stacks behave like simple lists: callers push a value before
entering a temporary context and pop it when leaving. Several
convenience context managers are provided to activate/deactivate
tenant-related backends when switching contexts.
"""
_tenant_stack = ContextVar("tenant_stack", default=[])
_db_alias_stack = ContextVar("db_alias_stack", default=[settings.MASTER_DB_ALIAS])
_cache_alias_stack = ContextVar(
"cache_alias_stack", default=[settings.MASTER_DB_ALIAS]
)
# --- Tenant ---
@classmethod
def get_tenant(cls) -> Optional[BaseTenant]:
"""Return the current tenant or ``None`` if no tenant set.
The current tenant is the top element of the tenant stack for the
current context.
"""
stack = cls._tenant_stack.get()
return stack[-1] if stack else None
@classmethod
def push_tenant(cls, tenant: BaseTenant):
"""Push ``tenant`` onto the current context's tenant stack.
Args:
tenant: an instance of :class:`BaseTenant` to become active.
"""
stack = cls._tenant_stack.get()
new_stack = stack + [tenant]
cls._tenant_stack.set(new_stack)
@classmethod
def pop_tenant(cls):
"""Remove the top tenant from the current context's tenant stack.
This is a no-op when the stack is already empty.
"""
stack = cls._tenant_stack.get()
if stack:
new_stack = stack[:-1]
cls._tenant_stack.set(new_stack)
# --- Database ---
@classmethod
def get_db_alias(cls):
"""Return the active database alias for the current context.
Falls back to ``settings.PUBLIC_DB_ALIAS`` if the stack is empty.
"""
stack = cls._db_alias_stack.get()
return stack[-1] if stack else settings.PUBLIC_DB_ALIAS
@classmethod
def push_db_alias(cls, db_alias):
"""Push a database alias onto the current context's DB alias stack.
Args:
db_alias: a string representing the Django database alias.
"""
stack = cls._db_alias_stack.get()
new_stack = stack + [db_alias]
cls._db_alias_stack.set(new_stack)
@classmethod
def pop_db_alias(cls):
"""Pop the current database alias from the DB alias stack.
No-op if the stack is empty.
"""
stack = cls._db_alias_stack.get()
if stack:
new_stack = stack[:-1]
cls._db_alias_stack.set(new_stack)
# --- Cache ---
@classmethod
def get_cache_alias(cls):
"""Return the active cache alias for the current context.
Falls back to the Django ``default`` cache when the stack is empty.
"""
stack = cls._cache_alias_stack.get()
return stack[-1] if stack else "default"
@classmethod
def push_cache_alias(cls, cache_alias):
"""Push a cache alias onto the current context's cache alias stack.
Args:
cache_alias: a string representing the cache alias to use.
"""
stack = cls._cache_alias_stack.get()
new_stack = stack + [cache_alias]
cls._cache_alias_stack.set(new_stack)
@classmethod
def pop_cache_alias(cls):
"""Pop the current cache alias from the cache alias stack.
No-op if the stack is empty.
"""
stack = cls._cache_alias_stack.get()
if stack:
new_stack = stack[:-1]
cls._cache_alias_stack.set(new_stack)
# --- Clear all (reset to defaults) ---
@classmethod
def clear_all(cls):
"""Reset all context stacks to their default values.
This sets an empty tenant stack and restores the DB/cache alias
stacks to the public/master defaults defined in settings.
"""
cls._tenant_stack.set([])
cls._db_alias_stack.set([settings.PUBLIC_DB_ALIAS])
cls._cache_alias_stack.set(["default"])
# --- Context manager ---
@classmethod
@contextmanager
def use_tenant(cls, tenant):
"""Context manager that activates tenant-specific backends.
This helper performs the following steps:
1. Pushes ``tenant`` onto the tenant stack.
2. Activates the appropriate database/schema backend for the tenant
and pushes the resulting DB alias.
3. Activates the cache backend for the tenant and pushes the cache
alias.
Upon exit the backends are deactivated and the pushed values are
popped from their respective stacks.
Args:
tenant: a :class:`BaseTenant` instance to activate.
"""
from django_omnitenant.backends.cache_backend import CacheTenantBackend
from django_omnitenant.backends.database_backend import DatabaseTenantBackend
from django_omnitenant.backends.schema_backend import SchemaTenantBackend
# Push tenant
cls.push_tenant(tenant)
# Activate DB/Schema backend
backend = (
SchemaTenantBackend(tenant)
if tenant.isolation_type == BaseTenant.IsolationType.SCHEMA
else DatabaseTenantBackend(tenant)
)
backend.activate()
cls.push_db_alias(cls.get_db_alias()) # backend may change alias
# Activate cache backend
cache_backend = CacheTenantBackend(tenant)
cache_backend.activate()
cls.push_cache_alias(cls.get_cache_alias())
try:
yield
finally:
# Deactivate backends
backend.deactivate()
cache_backend.deactivate()
# Pop tenant/db/cache
cls.pop_tenant()
cls.pop_db_alias()
cls.pop_cache_alias()
@classmethod
@contextmanager
def use_schema(cls, schema_name: str):
"""Temporarily switch to an existing schema by name.
This constructs a lightweight mock tenant object for the provided
``schema_name``, activates the schema backend and yields control to
the caller. On exit the backend is deactivated and the DB alias
is popped.
Args:
schema_name: the schema name to switch to (usually a string).
"""
from django_omnitenant.backends.schema_backend import SchemaTenantBackend
tenant: BaseTenant = get_tenant_model()(tenant_id=schema_name) # type: ignore # Mock tenant for context
backend = SchemaTenantBackend(tenant)
backend.activate()
try:
yield
finally:
backend.deactivate()
cls.pop_db_alias()
@classmethod
@contextmanager
def use_master_db(cls):
"""Context manager that temporarily switches to the master DB.
This is useful for operations that must run against the primary
(master) database and its corresponding cache. It pushes the
master DB/cache aliases, activates the default backends and
restores the previous state on exit.
"""
from django_omnitenant.backends.cache_backend import CacheTenantBackend
from django_omnitenant.backends.database_backend import DatabaseTenantBackend
# Push default DB & cache
master_db = settings.MASTER_DB_ALIAS
master_cache = settings.MASTER_DB_ALIAS
cls.push_db_alias(master_db)
cls.push_cache_alias(master_cache)
# Activate default backends
tenant: BaseTenant = get_tenant_model()(tenant_id=settings.PUBLIC_TENANT_NAME)
db_backend = DatabaseTenantBackend(tenant) # None means no specific tenant
db_backend.activate()
cache_backend = CacheTenantBackend(tenant)
cache_backend.activate()
try:
yield
finally:
db_backend.deactivate()
cache_backend.deactivate()
cls.pop_db_alias()
cls.pop_cache_alias()
# --- New: use public schema ---
@classmethod
@contextmanager
def use_public_schema(cls):
"""Activate the public (shared) schema and cache for the context.
This constructs a mock tenant representing the public schema,
activates the schema & cache backends and pushes the resulting
DB/cache aliases. On exit the backends are deactivated and the
aliases are popped.
"""
from django_omnitenant.backends.cache_backend import CacheTenantBackend
from django_omnitenant.backends.schema_backend import SchemaTenantBackend
# Create a mock tenant representing public schema
tenant: BaseTenant = get_tenant_model()(tenant_id="public") # type: ignore
backend = SchemaTenantBackend(tenant)
backend.activate()
cls.push_db_alias(cls.get_db_alias())
# Public cache
cache_backend = CacheTenantBackend(tenant)
cache_backend.activate()
cls.push_cache_alias(cls.get_cache_alias())
try:
yield
finally:
backend.deactivate()
cache_backend.deactivate()
cls.pop_db_alias()
cls.pop_cache_alias()
|
get_tenant()
classmethod
Return the current tenant or None if no tenant set.
The current tenant is the top element of the tenant stack for the
current context.
Source code in django_omnitenant/tenant_context.py
54
55
56
57
58
59
60
61
62
63 | @classmethod
def get_tenant(cls) -> Optional[BaseTenant]:
"""Return the current tenant or ``None`` if no tenant set.
The current tenant is the top element of the tenant stack for the
current context.
"""
stack = cls._tenant_stack.get()
return stack[-1] if stack else None
|
push_tenant(tenant)
classmethod
Push tenant onto the current context's tenant stack.
Parameters:
| Name |
Type |
Description |
Default |
tenant
|
BaseTenant
|
an instance of :class:BaseTenant to become active.
|
required
|
Source code in django_omnitenant/tenant_context.py
65
66
67
68
69
70
71
72
73
74
75 | @classmethod
def push_tenant(cls, tenant: BaseTenant):
"""Push ``tenant`` onto the current context's tenant stack.
Args:
tenant: an instance of :class:`BaseTenant` to become active.
"""
stack = cls._tenant_stack.get()
new_stack = stack + [tenant]
cls._tenant_stack.set(new_stack)
|
pop_tenant()
classmethod
Remove the top tenant from the current context's tenant stack.
This is a no-op when the stack is already empty.
Source code in django_omnitenant/tenant_context.py
77
78
79
80
81
82
83
84
85
86
87 | @classmethod
def pop_tenant(cls):
"""Remove the top tenant from the current context's tenant stack.
This is a no-op when the stack is already empty.
"""
stack = cls._tenant_stack.get()
if stack:
new_stack = stack[:-1]
cls._tenant_stack.set(new_stack)
|
get_db_alias()
classmethod
Return the active database alias for the current context.
Falls back to settings.PUBLIC_DB_ALIAS if the stack is empty.
Source code in django_omnitenant/tenant_context.py
90
91
92
93
94
95
96
97
98 | @classmethod
def get_db_alias(cls):
"""Return the active database alias for the current context.
Falls back to ``settings.PUBLIC_DB_ALIAS`` if the stack is empty.
"""
stack = cls._db_alias_stack.get()
return stack[-1] if stack else settings.PUBLIC_DB_ALIAS
|
push_db_alias(db_alias)
classmethod
Push a database alias onto the current context's DB alias stack.
Parameters:
| Name |
Type |
Description |
Default |
db_alias
|
|
a string representing the Django database alias.
|
required
|
Source code in django_omnitenant/tenant_context.py
100
101
102
103
104
105
106
107
108
109
110 | @classmethod
def push_db_alias(cls, db_alias):
"""Push a database alias onto the current context's DB alias stack.
Args:
db_alias: a string representing the Django database alias.
"""
stack = cls._db_alias_stack.get()
new_stack = stack + [db_alias]
cls._db_alias_stack.set(new_stack)
|
pop_db_alias()
classmethod
Pop the current database alias from the DB alias stack.
No-op if the stack is empty.
Source code in django_omnitenant/tenant_context.py
112
113
114
115
116
117
118
119
120
121
122 | @classmethod
def pop_db_alias(cls):
"""Pop the current database alias from the DB alias stack.
No-op if the stack is empty.
"""
stack = cls._db_alias_stack.get()
if stack:
new_stack = stack[:-1]
cls._db_alias_stack.set(new_stack)
|
get_cache_alias()
classmethod
Return the active cache alias for the current context.
Falls back to the Django default cache when the stack is empty.
Source code in django_omnitenant/tenant_context.py
125
126
127
128
129
130
131
132
133 | @classmethod
def get_cache_alias(cls):
"""Return the active cache alias for the current context.
Falls back to the Django ``default`` cache when the stack is empty.
"""
stack = cls._cache_alias_stack.get()
return stack[-1] if stack else "default"
|
push_cache_alias(cache_alias)
classmethod
Push a cache alias onto the current context's cache alias stack.
Parameters:
| Name |
Type |
Description |
Default |
cache_alias
|
|
a string representing the cache alias to use.
|
required
|
Source code in django_omnitenant/tenant_context.py
135
136
137
138
139
140
141
142
143
144
145 | @classmethod
def push_cache_alias(cls, cache_alias):
"""Push a cache alias onto the current context's cache alias stack.
Args:
cache_alias: a string representing the cache alias to use.
"""
stack = cls._cache_alias_stack.get()
new_stack = stack + [cache_alias]
cls._cache_alias_stack.set(new_stack)
|
pop_cache_alias()
classmethod
Pop the current cache alias from the cache alias stack.
No-op if the stack is empty.
Source code in django_omnitenant/tenant_context.py
147
148
149
150
151
152
153
154
155
156
157 | @classmethod
def pop_cache_alias(cls):
"""Pop the current cache alias from the cache alias stack.
No-op if the stack is empty.
"""
stack = cls._cache_alias_stack.get()
if stack:
new_stack = stack[:-1]
cls._cache_alias_stack.set(new_stack)
|
clear_all()
classmethod
Reset all context stacks to their default values.
This sets an empty tenant stack and restores the DB/cache alias
stacks to the public/master defaults defined in settings.
Source code in django_omnitenant/tenant_context.py
160
161
162
163
164
165
166
167
168
169
170 | @classmethod
def clear_all(cls):
"""Reset all context stacks to their default values.
This sets an empty tenant stack and restores the DB/cache alias
stacks to the public/master defaults defined in settings.
"""
cls._tenant_stack.set([])
cls._db_alias_stack.set([settings.PUBLIC_DB_ALIAS])
cls._cache_alias_stack.set(["default"])
|
use_tenant(tenant)
classmethod
Context manager that activates tenant-specific backends.
This helper performs the following steps:
- Pushes
tenant onto the tenant stack.
- Activates the appropriate database/schema backend for the tenant
and pushes the resulting DB alias.
- Activates the cache backend for the tenant and pushes the cache
alias.
Upon exit the backends are deactivated and the pushed values are
popped from their respective stacks.
Parameters:
| Name |
Type |
Description |
Default |
tenant
|
|
a :class:BaseTenant instance to activate.
|
required
|
Source code in django_omnitenant/tenant_context.py
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224 | @classmethod
@contextmanager
def use_tenant(cls, tenant):
"""Context manager that activates tenant-specific backends.
This helper performs the following steps:
1. Pushes ``tenant`` onto the tenant stack.
2. Activates the appropriate database/schema backend for the tenant
and pushes the resulting DB alias.
3. Activates the cache backend for the tenant and pushes the cache
alias.
Upon exit the backends are deactivated and the pushed values are
popped from their respective stacks.
Args:
tenant: a :class:`BaseTenant` instance to activate.
"""
from django_omnitenant.backends.cache_backend import CacheTenantBackend
from django_omnitenant.backends.database_backend import DatabaseTenantBackend
from django_omnitenant.backends.schema_backend import SchemaTenantBackend
# Push tenant
cls.push_tenant(tenant)
# Activate DB/Schema backend
backend = (
SchemaTenantBackend(tenant)
if tenant.isolation_type == BaseTenant.IsolationType.SCHEMA
else DatabaseTenantBackend(tenant)
)
backend.activate()
cls.push_db_alias(cls.get_db_alias()) # backend may change alias
# Activate cache backend
cache_backend = CacheTenantBackend(tenant)
cache_backend.activate()
cls.push_cache_alias(cls.get_cache_alias())
try:
yield
finally:
# Deactivate backends
backend.deactivate()
cache_backend.deactivate()
# Pop tenant/db/cache
cls.pop_tenant()
cls.pop_db_alias()
cls.pop_cache_alias()
|
use_schema(schema_name)
classmethod
Temporarily switch to an existing schema by name.
This constructs a lightweight mock tenant object for the provided
schema_name, activates the schema backend and yields control to
the caller. On exit the backend is deactivated and the DB alias
is popped.
Parameters:
| Name |
Type |
Description |
Default |
schema_name
|
str
|
the schema name to switch to (usually a string).
|
required
|
Source code in django_omnitenant/tenant_context.py
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249 | @classmethod
@contextmanager
def use_schema(cls, schema_name: str):
"""Temporarily switch to an existing schema by name.
This constructs a lightweight mock tenant object for the provided
``schema_name``, activates the schema backend and yields control to
the caller. On exit the backend is deactivated and the DB alias
is popped.
Args:
schema_name: the schema name to switch to (usually a string).
"""
from django_omnitenant.backends.schema_backend import SchemaTenantBackend
tenant: BaseTenant = get_tenant_model()(tenant_id=schema_name) # type: ignore # Mock tenant for context
backend = SchemaTenantBackend(tenant)
backend.activate()
try:
yield
finally:
backend.deactivate()
cls.pop_db_alias()
|
use_master_db()
classmethod
Context manager that temporarily switches to the master DB.
This is useful for operations that must run against the primary
(master) database and its corresponding cache. It pushes the
master DB/cache aliases, activates the default backends and
restores the previous state on exit.
Source code in django_omnitenant/tenant_context.py
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285 | @classmethod
@contextmanager
def use_master_db(cls):
"""Context manager that temporarily switches to the master DB.
This is useful for operations that must run against the primary
(master) database and its corresponding cache. It pushes the
master DB/cache aliases, activates the default backends and
restores the previous state on exit.
"""
from django_omnitenant.backends.cache_backend import CacheTenantBackend
from django_omnitenant.backends.database_backend import DatabaseTenantBackend
# Push default DB & cache
master_db = settings.MASTER_DB_ALIAS
master_cache = settings.MASTER_DB_ALIAS
cls.push_db_alias(master_db)
cls.push_cache_alias(master_cache)
# Activate default backends
tenant: BaseTenant = get_tenant_model()(tenant_id=settings.PUBLIC_TENANT_NAME)
db_backend = DatabaseTenantBackend(tenant) # None means no specific tenant
db_backend.activate()
cache_backend = CacheTenantBackend(tenant)
cache_backend.activate()
try:
yield
finally:
db_backend.deactivate()
cache_backend.deactivate()
cls.pop_db_alias()
cls.pop_cache_alias()
|
use_public_schema()
classmethod
Activate the public (shared) schema and cache for the context.
This constructs a mock tenant representing the public schema,
activates the schema & cache backends and pushes the resulting
DB/cache aliases. On exit the backends are deactivated and the
aliases are popped.
Source code in django_omnitenant/tenant_context.py
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319 | @classmethod
@contextmanager
def use_public_schema(cls):
"""Activate the public (shared) schema and cache for the context.
This constructs a mock tenant representing the public schema,
activates the schema & cache backends and pushes the resulting
DB/cache aliases. On exit the backends are deactivated and the
aliases are popped.
"""
from django_omnitenant.backends.cache_backend import CacheTenantBackend
from django_omnitenant.backends.schema_backend import SchemaTenantBackend
# Create a mock tenant representing public schema
tenant: BaseTenant = get_tenant_model()(tenant_id="public") # type: ignore
backend = SchemaTenantBackend(tenant)
backend.activate()
cls.push_db_alias(cls.get_db_alias())
# Public cache
cache_backend = CacheTenantBackend(tenant)
cache_backend.activate()
cls.push_cache_alias(cls.get_cache_alias())
try:
yield
finally:
backend.deactivate()
cache_backend.deactivate()
cls.pop_db_alias()
cls.pop_cache_alias()
|