"""
Simple translation of Django model classes to peewee model classes.
"""
from functools import partial
import logging
from peewee import *
logger = logging.getLogger('peewee.playhouse.djpeewee')
class AttrDict(dict):
def __getattr__(self, attr):
return self[attr]
class DjangoTranslator(object):
def __init__(self):
self._field_map = self.get_django_field_map()
def get_django_field_map(self):
from django.db.models import fields as djf
return [
(djf.AutoField, PrimaryKeyField),
(djf.BigIntegerField, BigIntegerField),
# (djf.BinaryField, BlobField),
(djf.BooleanField, BooleanField),
(djf.CharField, CharField),
(djf.DateTimeField, DateTimeField), # Extends DateField.
(djf.DateField, DateField),
(djf.DecimalField, DecimalField),
(djf.FilePathField, CharField),
(djf.FloatField, FloatField),
(djf.IntegerField, IntegerField),
(djf.NullBooleanField, partial(BooleanField, null=True)),
(djf.TextField, TextField),
(djf.TimeField, TimeField),
(djf.related.ForeignKey, ForeignKeyField),
]
def convert_field(self, field):
converted = None
for django_field, peewee_field in self._field_map:
if isinstance(field, django_field):
converted = peewee_field
break
return converted
def _translate_model(self,
model,
mapping,
max_depth=None,
backrefs=False,
exclude=None):
if exclude and model in exclude:
return
if max_depth is None:
max_depth = -1
from django.db.models import fields as djf
options = model._meta
if mapping.get(options.object_name):
return
mapping[options.object_name] = None
attrs = {}
# Sort fields such that nullable fields appear last.
field_key = lambda field: (field.null and 1 or 0, field)
for model_field in sorted(options.fields, key=field_key):
# Get peewee equivalent for this field type.
converted = self.convert_field(model_field)
# Special-case ForeignKey fields.
if converted is ForeignKeyField:
if max_depth != 0:
related_model = model_field.rel.to
model_name = related_model._meta.object_name
# If we haven't processed the related model yet, do so now.
if model_name not in mapping:
mapping[model_name] = None # Avoid endless recursion.
self._translate_model(
related_model,
mapping,
max_depth=max_depth - 1,
backrefs=backrefs,
exclude=exclude)
if mapping[model_name] is None:
# Cycle detected, put an integer field here.
logger.warn('Cycle detected: %s: %s',
model_field.name, model_name)
attrs[model_field.name] = IntegerField(
db_column=model_field.column)
else:
related_name = (model_field.rel.related_name or
model_field.related_query_name())
if related_name.endswith('+'):
related_name = '__%s:%s:%s' % (
options,
model_field.name,
related_name.strip('+'))
attrs[model_field.name] = ForeignKeyField(
mapping[model_name],
related_name=related_name,
db_column=model_field.column,
)
else:
attrs[model_field.name] = IntegerField(
db_column=model_field.column)
elif converted:
attrs[model_field.name] = converted(
db_column=model_field.db_column)
klass = type(options.object_name, (Model,), attrs)
klass._meta.db_table = options.db_table
klass._meta.database.interpolation = '%s'
mapping[options.object_name] = klass
if backrefs:
# Follow back-references for foreign keys.
try:
all_related = [(f, f.model) for f in
options.get_all_related_objects()]
except AttributeError:
all_related = [(f, f.field.model) for f in options.get_fields()
if (f.one_to_many or f.one_to_one)
and f.auto_created and not f.concrete]
for rel_obj, model in all_related:
if model._meta.object_name in mapping:
continue
self._translate_model(
model,
mapping,
max_depth=max_depth - 1,
backrefs=backrefs,
exclude=exclude)
# Load up many-to-many relationships.
for many_to_many in options.many_to_many:
if not isinstance(many_to_many, djf.related.ManyToManyField):
continue
self._translate_model(
many_to_many.rel.through,
mapping,
max_depth=max_depth, # Do not decrement.
backrefs=backrefs,
exclude=exclude)
def translate_models(self, *models, **options):
"""
Generate a group of peewee models analagous to the provided Django
models for the purposes of creating queries.
:param model: A Django model class.
:param options: A dictionary of options, see note below.
:returns: A dictionary mapping model names to peewee model classes.
:rtype: dict
Recognized options:
`recurse`: Follow foreign keys (default: True)
`max_depth`: Max depth to recurse (default: None, unlimited)
`backrefs`: Follow backrefs (default: False)
`exclude`: A list of models to exclude
Example::
# Map Django models to peewee models. Foreign keys and M2M will be
# traversed as well.
peewee = translate(Account)
# Generate query using peewee.
PUser = peewee['User']
PAccount = peewee['Account']
query = (PUser
.select()
.join(PAccount)
.where(PAccount.acct_type == 'foo'))
# Django raw query.
users = User.objects.raw(*query.sql())
"""
mapping = AttrDict()
recurse = options.get('recurse', True)
max_depth = options.get('max_depth', None)
backrefs = options.get('backrefs', False)
exclude = options.get('exclude', None)
if not recurse and max_depth:
raise ValueError('Error, you cannot specify a max_depth when '
'recurse=False.')
elif not recurse:
max_depth = 0
elif recurse and max_depth is None:
max_depth = -1
for model in models:
self._translate_model(
model,
mapping,
max_depth=max_depth,
backrefs=backrefs,
exclude=exclude)
return mapping
try:
import django
translate = DjangoTranslator().translate_models
except ImportError:
pass