import json
from datalookup import Dataset
from django.apps import apps, AppConfig
from django.db import models
from diskette.exceptions import (
DoesNotExistsFromAppstore, MultipleObjectExistsFromAppstore
)
[docs]
class DjangoAppLookupStore:
"""
Django application store collects applications and store them with their useful
parameters.
.. _Django apps: https://docs.djangoproject.com/en/stable/ref/applications/
This registry is built on top of `Django apps`_ to include more useful methods to
perform queries alike with Django ORM. It helps to find detailed inclusions and
exclusions with application models.
Attributes:
registry (Dataset): The store registry built as a ``datalookup.Dataset``.
Keyword Arguments:
registry (list): List of dictionnaries, each dictionnary is an application
item with its parameters. If not given, it will be filled automatically
from enabled applications using ``django.apps``.
"""
def __init__(self, registry=None):
self._registry = registry or self.get_registry()
self.registry = Dataset(self._registry)
[docs]
def as_dict(self):
"""
Return the store items with their parameters as a dictionnary.
Returns:
dict: Store items as a dictionnary.
"""
return self._registry
[docs]
def as_json(self):
"""
Return the store items with their parameters as JSON.
Returns:
string: Store items as JSON.
"""
return json.dumps(self.as_dict())
[docs]
@classmethod
def normalize_label(cls, model, app=None):
"""
Return normalized name for a model or FQM label.
Arguments:
model (string or object): Model name as a string or an object that inherits
from ``django.db.models.base.ModelBase``.
Keyword Arguments:
app (string or object): Either a model object, AppConfig object or a
string for an application label.
Returns:
string: A fully qualified model name composed from app name and model name.
"""
if app and isinstance(app, AppConfig):
app = app.label
if isinstance(model, models.base.ModelBase):
model = model.__name__
if not app:
return model
return app + "." + model
[docs]
@classmethod
def is_fully_qualified_labels(cls, labels):
"""
Validate given labels are fully qualified model labels.
Arguments:
labels (list): List of label to validate.
Returns:
list: A list of invalid labels.
"""
# NOTE: We currently don't bother of label that are divided (by a dot) in more
# than two parts, since we are not sure Django would allows labels on three or
# more parts
return [
pattern
for pattern in labels
if (
len(pattern.split(".")) < 2 or
pattern.startswith(".") or
pattern.endswith(".")
)
]
[docs]
def get_registry_app_models(self, app_id, app):
"""
Return model labels for given application.
Internally used to build appstore registry.
Arguments:
app_id (integer): An unique id to represent related application.
app (AppConfig): The application object from Django apps.
Returns:
list: A list of dictionnaries for models. Model dictionnary will contains
the following items:
id
A simple integer built from the loop, since it is only done internally
it should be ensured to be unique.
app_id
Given application id.
unique_id
A special field to help for ordering on models since datalookup does
not provide a way to do this. This is a join of app id and model id,
both formatted on 4 digits filled by zero.
app
Application label.
name
Model class name.
object
Model class.
label
Fully qualified model label.
"""
return [
{
"id": i,
"app_id": app_id,
"unique_id": "{app}{model}".format(
app=str(app_id).zfill(4),
model=str(i).zfill(4),
),
"app": app.label,
"name": model.__name__,
"object": model,
"label": self.normalize_label(model, app=app),
}
for i, model in enumerate(app.get_models(
include_auto_created=True,
include_swapped=True
), start=1)
]
[docs]
def get_registry(self):
"""
Store all model labels from enabled applications into a registry.
Returns:
list: A list of dictionnaries for applications. Application dictionnary
will contains the following items:
id
A simple integer built from the loop, since it is only done internally
it should be ensured to be unique.
verbose_name
Application verbose name.
label
Application label.
pythonpath
Python path to the application itself.
models
A list of application models as returned from method
``DjangoAppLookupStore.get_registry_app_models``.
"""
collected = []
i = 1
for app in apps.get_app_configs():
names = self.get_registry_app_models(i, app)
if names:
collected.append({
"id": i,
"verbose_name": str(app.verbose_name),
"label": app.label,
"pythonpath": app.name,
"models": names,
})
i += 1
return collected
[docs]
def get_app(self, label):
"""
Getter to retrieve a single application from a label.
Arguments:
label (string): Application label.
Raises:
MultipleObjectExistsFromAppstore: If there is more than one application
object with the same label. This is something that should never happen
because of how Django manage applications.
DoesNotExistsFromAppstore: If there is not any application with the given
label.
Returns:
node: Datalookup node for retrieved application.
"""
app = self.registry.filter(label__exact=label)
length = len(app)
if length > 1:
raise MultipleObjectExistsFromAppstore(
"Given app label '{app}' return multiple objects ({length})".format(
app=label,
length=length,
)
)
elif length == 0:
raise DoesNotExistsFromAppstore(
"No app object exists for given label: '{app}'".format(
app=label,
)
)
return app[0]
[docs]
def get_model(self, label):
"""
Getter to retrieve a single application model from a label.
Arguments:
label (string): Fully qualified model label.
Raises:
MultipleObjectExistsFromAppstore: If there is more than one application
object with the same label. This is something that should never happen
because of how Django manage applications.
DoesNotExistsFromAppstore: If there is not any application with the given
label.
Returns:
node: Datalookup node for retrieved application.
"""
model = self.registry.filter_related("models", label__exact=label)
length = len(model)
if length > 1:
raise MultipleObjectExistsFromAppstore(
"Given model label '{model}' return multiple objects ({length})".format(
model=label,
length=length,
)
)
elif length == 0:
raise DoesNotExistsFromAppstore(
"No model object exists for given label: '{model}'".format(
model=label,
)
)
return model[0]
[docs]
def get_app_model_labels(self, app):
"""
Return model labels for given application.
Arguments:
app (sring): Application label.
Returns:
list: Fully qualified model labels.
"""
return [
model.label
for model in self.registry.filter(
label__exact=app
).filter_related("models")
]
[docs]
def get_all_model_labels(self):
"""
Return all model labels from all enabled applications.
Returns:
list: Fully qualified model labels.
"""
return [
model.label
for model in self.registry.filter_related("models")
]
[docs]
def check_unexisting_labels(self, labels):
"""
Check if given labels exists as app or models in store.
Arguments:
labels (list): A list of application or FQM label (string) to check.
Returns:
Tuple: Respectively a list of not found apps and a list of not found
models.
"""
unknow_apps = []
unknow_models = []
# Ensure we allways have a list
labels = [labels] if isinstance(labels, str) else labels
for label in labels:
# Try to parse item as a fully qualified label
try:
app_label, model_label = label.split(".")
# If not a fully qualified label assume it is an application label and add
# all models in their original registered order
except ValueError:
try:
self.get_app(label)
except DoesNotExistsFromAppstore:
unknow_apps.append(label)
else:
try:
self.get_model(label)
except DoesNotExistsFromAppstore:
unknow_models.append(label)
return unknow_apps, unknow_models
[docs]
def get_models_inclusions(self, labels, excludes=None):
"""
Returns model nodes for inclusions.
Arguments:
labels (string or list): A list of App label (string), fully qualified
model labels (string) or AppConfig.
Keyword Arguments:
excludes (list): List of fully qualified model labels to exclude.
Returns:
list: List of Datalookup nodes.
"""
names = []
# Ensure we allways have a list
labels = [labels] if isinstance(labels, str) else labels
excludes = excludes or []
excludes = [excludes] if isinstance(excludes, str) else excludes
for label in labels:
if isinstance(label, AppConfig):
label = label.label
# Try to parse item as a fully qualified label
try:
app_label, model_label = label.split(".")
# If not a fully qualified label assume it is an application label and add
# all models in their original registered order
except ValueError:
# Append all models that are not excluded explicitely
names.extend([
model
for model in self.get_app(label).models
if model.label not in excludes
])
else:
# Add the fully qualified label if not excluded explicitely
if self.normalize_label(model_label, app=app_label) not in excludes:
names.append(self.get_model(label))
# Finally order on model unique id
return sorted(names, key=lambda item: item.unique_id)
[docs]
def get_models_exclusions(self, labels, excludes=None):
"""
Returns model nodes for exclusions.
Exclusion gather the explicit exclude labels and the intersection between
inclusions and missing models related to implied app from inclusion labels.
Arguments:
labels (string or list): App label or fully qualified model labels.
Keyword Arguments:
excludes (list): List of fully qualified model labels.
Returns:
list: List of Datalookup nodes.
"""
names = []
# Ensure we allways have a list
excludes = [excludes] if isinstance(excludes, str) else excludes
excludes = excludes or []
# Get model nodes for inclusion from given labels
model_items = self.get_models_inclusions(labels, excludes=excludes)
exclude_apps = [label.split(".")[0] for label in excludes]
# Get involved app labels from resolved inclusions and exclusions, enforcing
# unique items
involved_apps = list(set([item.app for item in model_items] + exclude_apps))
# Queryset is related to models but limited on involved apps
q = self.registry.filter(label__in=involved_apps).filter_related("models")
# Finally reject inclusions
names = q.exclude(label__in=[v.label for v in model_items])
return sorted(names, key=lambda item: item.unique_id)