Classical mapping in MongoEngine - python

I'm new to MongoEngine and it looks like we need to create sub classes of the class Document from the mongoengine to model our DB. I'm a little concerned here because this violates the Dependency Inversion from the SOLID principles. So if I need to use another database at a later point of time, I will have to change my domain model classes which I shouldn't really be doing.
SQLAlchemy overcomes this by providing a beautiful classical mapping. Using this, the database dependent code is separated from my domain model, so I don't really need to worry about the database provider and I can easily abstract the details away should I have a need to change my database.
Is there a equivalent of this for MongoDB, preferrably in MongoEngine?

Pymongo's official doc provides a list of the existing ORM/ODM and frameworks but to my knowledge they all implement the Active Record Pattern (just like django ORM), which as you said, violates the SOLID principles but is good enough for many simple use cases.
MongoAlchemy, which was inspired by SQLAlchemy uses a concept of session so it may be closer to what you are looking for but the project is no longer maintained.

If I understand correctly, you're trying to map an object to document schema using mongoengine.
Let's create a document class for a user:
from mongoengine import Document, StringField
class UserDocument(Document):
username = StringField(required=True)
password = StringField(required=True)
email = StringField(required=True)
Now add a class method that creates new users:
from mongoengine import disconnect, connect, Document, StringField
class UserDocument(Document):
username = StringField(required=True)
password = StringField(required=True)
email = StringField(required=True)
#classmethod
def new(cls):
data = UserDocument(username=cls.username, password=cls.password, email=cls.email)
connect('test_collection')
data.save()
disconnect('test_collection')
As I understand your question, your issue in this example is that UserDocument would be aware of mongoengine thus violating the dependency inversion principle. This can be solved with a child class.
First allow inheritance in UserDocument:
...
class UserDocument(Document):
meta = {'allow_inheritance': True}
username = StringField(required=True)
...
Next we build the child:
from user_document import UserDocument
# Maps object to schema
class User(UserDocument):
def __init__(self, *args, **values):
super().__init__(*args, **values)
Next add a create method:
from user_document import UserDocument
# Maps object to schema
class User(UserDocument):
def __init__(self, *args, **values):
super().__init__(*args, **values)
def create(self, username, password, email):
self.username, self.password, self.email = username, password, email
User.new()
Now our User object inherits the UserDocument fields. UserDocument.new can be accessed directly or through the child with User.new().
from model import User
username, password, email = 'cool username', 'super secret password', 'mrcool#example.com'
User.create(User, username, password, email)
The User object is aware of UserDocument which in turn depends on mongoengine.
I apologize if I misunderstood or used incorrect vocabulary to describe the example solution. I'm relatively new, self-taught, and have no friends who code which makes discussion difficult.

This topic is covered in the first 6 chapters of CosmicPython/Architecture Patterns With Python.
However, in those chapters it uses SQLAlchemy with mappers.
The book does have a section with an example for other ORMs that use an ActiveRecord style - like mongoengine - in
Appendix D: Repository and Unit of Work Patterns with Django.
First the models are defined.
Please note the following example may be hard to follow without any background and so I recommend reading the first 6 chapters of CosmicPython if the example below is unclear.
src/djangoproject/alloc/models.py
from django.db import models
from allocation.domain import model as domain_model
class Batch(models.Model):
reference = models.CharField(max_length=255)
sku = models.CharField(max_length=255)
qty = models.IntegerField()
eta = models.DateField(blank=True, null=True)
#staticmethod
def update_from_domain(batch: domain_model.Batch):
try:
b = Batch.objects.get(reference=batch.reference)
except Batch.DoesNotExist:
b = Batch(reference=batch.reference)
b.sku = batch.sku
b.qty = batch._purchased_quantity
b.eta = batch.eta
b.save()
b.allocation_set.set(
Allocation.from_domain(l, b)
for l in batch._allocations
)
def to_domain(self) -> domain_model.Batch:
b = domain_model.Batch(
ref=self.reference, sku=self.sku, qty=self.qty, eta=self.eta
)
b._allocations = set(
a.line.to_domain()
for a in self.allocation_set.all()
)
return b
class OrderLine(models.Model):
orderid = models.CharField(max_length=255)
sku = models.CharField(max_length=255)
qty = models.IntegerField()
def to_domain(self):
return domain_model.OrderLine(
orderid=self.orderid, sku=self.sku, qty=self.qty
)
#staticmethod
def from_domain(line):
l, _ = OrderLine.objects.get_or_create(
orderid=line.orderid, sku=line.sku, qty=line.qty
)
return l
class Allocation(models.Model):
batch = models.ForeignKey(Batch, on_delete=models.CASCADE)
line = models.ForeignKey(OrderLine, on_delete=models.CASCADE)
#staticmethod
def from_domain(domain_line, django_batch):
a, _ = Allocation.objects.get_or_create(
line=OrderLine.from_domain(domain_line),
batch=django_batch,
)
return a
Then a port and adapter are defined for the repository pattern in
src/allocation/adapters/repository.py
# pylint: disable=no-member, no-self-use
from typing import Set
import abc
from allocation.domain import model
from djangoproject.alloc import models as django_models
class AbstractRepository(abc.ABC):
def __init__(self):
self.seen = set() # type: Set[model.Batch]
def add(self, batch: model.Batch):
self.seen.add(batch)
def get(self, reference) -> model.Batch:
p = self._get(reference)
if p:
self.seen.add(p)
return p
#abc.abstractmethod
def _get(self, reference):
raise NotImplementedError
class DjangoRepository(AbstractRepository):
def add(self, batch):
super().add(batch)
self.update(batch)
def update(self, batch):
django_models.Batch.update_from_domain(batch)
def _get(self, reference):
return (
django_models.Batch.objects.filter(reference=reference)
.first()
.to_domain()
)
def list(self):
return [b.to_domain() for b in django_models.Batch.objects.all()]
Along with the domain models
src/allocation/domain/model.py
from __future__ import annotations
from dataclasses import dataclass
from datetime import date
from typing import Optional, List, Set
class OutOfStock(Exception):
pass
def allocate(line: OrderLine, batches: List[Batch]) -> str:
try:
batch = next(b for b in sorted(batches) if b.can_allocate(line))
batch.allocate(line)
return batch.reference
except StopIteration:
raise OutOfStock(f"Out of stock for sku {line.sku}")
#dataclass(unsafe_hash=True)
class OrderLine:
orderid: str
sku: str
qty: int
class Batch:
def __init__(self, ref: str, sku: str, qty: int, eta: Optional[date]):
self.reference = ref
self.sku = sku
self.eta = eta
self._purchased_quantity = qty
self._allocations = set() # type: Set[OrderLine]
def __repr__(self):
return f"<Batch {self.reference}>"
def __eq__(self, other):
if not isinstance(other, Batch):
return False
return other.reference == self.reference
def __hash__(self):
return hash(self.reference)
def __gt__(self, other):
if self.eta is None:
return False
if other.eta is None:
return True
return self.eta > other.eta
def allocate(self, line: OrderLine):
if self.can_allocate(line):
self._allocations.add(line)
def deallocate(self, line: OrderLine):
if line in self._allocations:
self._allocations.remove(line)
#property
def allocated_quantity(self) -> int:
return sum(line.qty for line in self._allocations)
#property
def available_quantity(self) -> int:
return self._purchased_quantity - self.allocated_quantity
def can_allocate(self, line: OrderLine) -> bool:
return self.sku == line.sku and self.available_quantity >= line.qty

Related

Combine SQLAlchemy hybrid_property with native property construction

I have a User class in SQLAlchemy. I want to be able to encrypt the user's email address attribute in the database but still make it searchable through the filter query.
My problem is that if I use #hybrid_property my query theoretically works, but my construction doesn't, and if I use #property my construction works but my query doesn't
from cryptography.fernet import Fernet # <- pip install cryptography
from werkzeug.security import generate_password_hash
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
email_hash = db.Column(db.String(184), unique=True, nullable=False)
password_hash = db.Column(db.String(128))
# #property # <- Consider this as option 2...
#hybrid_property # <- Consider this as option 1...
def email(self):
f = Fernet('SOME_ENC_KEY')
value = f.decrypt(self.email_hash.encode('utf-8'))
return value
#email.setter
def email(self, email):
f = Fernet('SOME_ENC_KEY')
self.email_hash = f.encrypt(email.encode('utf-8'))
#property
def password(self):
raise AttributeError('password is not a readable attribute.')
#password.setter
def password(self, password):
self.password_hash = generate_password_hash(password)
def __init__(self, **kwargs):
super(User, self).__init__(**kwargs)
# other checks and modifiers
For option 1: When I attempt to construct a user with User(email='a#example.com',password='secret') I receive the traceback,
~/models.py in __init__(self, **kwargs)
431 # Established role assignment by default class initiation
432 def __init__(self, **kwargs):
--> 433 super(User, self).__init__(**kwargs)
434 if self.role is None:
435 _default_role = Role.query.filter_by(default=True).first()
~/lib/python3.6/site-packages/sqlalchemy/ext/declarative/base.py in _declarative_constructor(self, **kwargs)
697 raise TypeError(
698 "%r is an invalid keyword argument for %s" %
--> 699 (k, cls_.__name__))
700 setattr(self, k, kwargs[k])
701 _declarative_constructor.__name__ = '__init__'
TypeError: 'email' is an invalid keyword argument for User
For option 2: If instead I change #hybrid_property to #property the construction is fine but then my query User.query.filter_by(email=form.email.data.lower()).first() fails and returns None.
What should I change to get it working as required?
==============
Note I should say that I have tried to avoid using dual attributes since I didn't want to make extensive edits to the underlying codebase. so I have explicitly tried to avoid separating creation with querying in terms of User(email_input='a#a.com', password='secret') and User.query.filter_by(email='a#a.com').first():
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
email_hash = db.Column(db.String(184), unique=True, nullable=False)
password_hash = db.Column(db.String(128))
#hybrid_property
def email(self):
f = Fernet('SOME_ENC_KEY')
value = f.decrypt(self.email_hash.encode('utf-8'))
return value
#property
def email_input(self):
raise AttributeError('email_input is not a readable attribute.')
#email_input.setter
def email_input(self, email):
f = Fernet('SOME_ENC_KEY')
self.email_hash = f.encrypt(email.encode('utf-8'))
#property
def password(self):
raise AttributeError('password is not a readable attribute.')
#password.setter
def password(self, password):
self.password_hash = generate_password_hash(password)
def __init__(self, **kwargs):
super(User, self).__init__(**kwargs)
# other checks and modifiers
In your hybrid_property, email, the line self.f.decrypt(self.email_hash.encode('utf-8')) is fine if self.email_hash is a str type, however, as email is a hybrid_property, when SQLAlchemy uses it to generate SQL self.email_hash is actually a sqlalchemy.orm.attributes.InstrumentedAttribute type.
From the docs regarding hybrid properties:
In many cases, the construction of an in-Python function and a
SQLAlchemy SQL expression have enough differences that two separate
Python expressions should be defined.
And so you can define an hybrid_property.expression method which is what SQLAlchemy will use to generate sql, allowing you to keep your string treatment intact in your hybrid_property method.
Here is the code I ended up with that worked for me given your example. I've stripped quite a bit out of your User model for simplicity but all the important parts are there. I also had to make up implementations for other functions/classes that were called in your code but not supplied (see MCVE):
class Fernet:
def __init__(self, k):
self.k = k
def encrypt(self, s):
return s
def decrypt(self, s):
return s
def get_env_variable(s):
return s
def generate_password_hash(s):
return s
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
email_hash = db.Column(db.String(184), unique=True, nullable=False)
f = Fernet(get_env_variable('FERNET_KEY'))
#hybrid_property
def email(self):
return self.f.decrypt(self.email_hash.encode('utf-8'))
#email.expression
def email(cls):
return cls.f.decrypt(cls.email_hash)
#email.setter
def email(self, email):
self.email_hash = self.f.encrypt(email.encode('utf-8'))
if __name__ == '__main__':
db.drop_all()
db.create_all()
u = User(email='a#example.com')
db.session.add(u)
db.session.commit()
print(User.query.filter_by(email='a#example.com').first())
# <User 1>
Unfortunately, the code above only works because the mock Fernet.decrypt method returns the exact object that was passed in. The problem with storing a Fernet encoded hash of the user's email addresses is that Fernet.encrypt does not return the same fernet token from one execution to the next, even with the same key. E.g.:
>>> from cryptography.fernet import Fernet
>>> f = Fernet(Fernet.generate_key())
>>> f.encrypt('a#example.com'.encode('utf-8')) == f.encrypt('a#example.com'.encode('utf-8'))
False
So, you want to query a database for a record, but with no way of knowing what the stored value of field that you are querying actually is at query time. You could build a classmethod that queries the entire users table and loop through each record, decrypting it's stored hash and comparing it to the clear text email. Or you can build a hashing function that will always return the same value, hash new users emails using that function and query the email_hash field directly with the hash of the email string. Of those, the first would be very inefficient given lots of users.
The Fernet.encrypt function is:
def encrypt(self, data):
current_time = int(time.time())
iv = os.urandom(16)
return self._encrypt_from_parts(data, current_time, iv)
So, you could define static values of current_time and iv and directly call Fermat._encrypt_from_parts yourself. Or you could use python's built in hash and just set a fixed seed so that it is deterministic. You could then hash the email string that you want to query and first and directly query Users.email_hash. As long as you didn't do any of the above for password fields!

Django - Custom Authentication & correct way to use Managers

I've hacked my way to getting my code to work, but I'm pretty sure I'm not doing it as it was intended.
My constraint is I want to have separate DB and UI layers, so I have all the DB-logic encapsulated in SPs/functions that are called from Django's view layer. I tried doing this using the included managers, but kept getting this error:
Manager isn't accessible via %s instances" % cls.__name__)
So, I just removed the manager sub-class and kept going. It works with some extra hacks, but it doesn't feel right. My question is, how do I get my code to work, but still inheriting the stuff from the appropriate managers (i.e. BaseUserManager)? Here's the code:
models.py
from __future__ import unicode_literals
from django.db import models
from UsefulFunctions.dbUtils import *
from django.contrib.auth.models import AbstractBaseUser
class MyUserManager():
# Bypassing BaseUserManager which includes these methods: normalize_email, make_random_password, get_by_natural_key
# Create new user
def create_user(self, password, usertype = None, firstname = None, lastname = None, phonenumber = None, emailaddress = None):
user = MyUser( # TO-DO: Replace MyUser with "get_user_model" reference
userid=None,
usertype=usertype,
firstname=firstname,
lastname=lastname,
phonenumber=phonenumber,
emailaddress=emailaddress
)
# Hash and save password
user.set_password(password)
# Save user data
user.save()
return user
def upsertUser(self, myUser):
return saveDBData('SP_IGLUpsertUser',
(
myUser.userid,
myUser.usertype,
myUser.firstname,
myUser.lastname,
myUser.phonenumber,
myUser.emailaddress,
myUser.password,
myUser.last_login,
None,
)
)
# Create custom base user
class MyUser(AbstractBaseUser):
# Define attributes (inherited class includes password + other fields)
userid = models.IntegerField(unique=True)
usertype = models.CharField(max_length=2)
firstname = models.CharField(max_length=100)
lastname = models.CharField(max_length=100)
phonenumber = models.CharField(max_length=25)
emailaddress = models.CharField(max_length=250)
# Define data manager
MyUserMgr = MyUserManager()
# Create new constructor
def __init__(self, userid = None, usertype = None, firstname = None, lastname = None, phonenumber = None, emailaddress = None):
super(MyUser, self).__init__() # TO-DO: Convert MyUser to get_user_model()
self.userid = userid
self.usertype = usertype
self.firstname = firstname
self.lastname = lastname
self.phonenumber = phonenumber
self.emailaddress = emailaddress
# Define required fields for AbstractBaseUser class
USERNAME_FIELD = 'userid' # specify how Django recognizes the user
EMAIL_FIELD = 'emailaddress'
REQUIRED_FIELDS = ['usertype','firstname','lastname'] # email and password are required by default
# Define class meta info
class Meta:
managed = False
db_table = 'userprofile'
# Required methods
def get_full_name(self):
return self.firstname + " " + self.lastname + " (" + self.userid + ")"
def get_short_name(self):
return self.userid
def save(self):
return self.MyUserMgr.upsertUser(self)
# Define model managers (interface between DB and objects)
class ItemDataManager():
def getAllItems(self):
return getDBData('SP_IGLGetItem', (None,)) # Use tuple instead of array for input parameters
def getItem(self, myItem):
return getDBData('SP_IGLGetItem', (myItem.itemid,))
def getItemDetail(self, myItem):
return getDBData('SP_IGLGetItemDetail', (myItem.itemid,))
def upsertItem(self, myItem):
return saveDBData('SP_IGLUpsertItem',
(
myItem.itemid,
myItem.itemname,
myItem.itemdescription,
myItem.itemcontactuserid,
)
)
def deleteItem(self, myItem):
return deleteDBData('SP_IGLDeleteItem', (myItem.itemid, None))
# Define data models (i.e. tables)
class Item(models.Model):
# Model properties
itemid = models.IntegerField
itemname = models.CharField(max_length=100)
itemdescription = models.CharField(max_length=5000)
itemcontactuserid = models.IntegerField
# Create Item Data Manager instance
myItemMgr = ItemDataManager()
# Create new constructor
def __init__(self, itemid = None):
super(Item, self).__init__()
self.itemid = itemid
# Define static methods (don't depend on object instance)
#staticmethod
def get_all():
return ItemDataManager().getAllItems()
# Define instance methods
def get(self):
return self.myItemMgr.getItem(self)
# Define instance methods
def get_detail(self):
return self.myItemMgr.getItemDetail(self)
def save(self):
return self.myItemMgr.upsertItem(self)
def delete(self):
return self.myItemMgr.deleteItem(self)
Sample call:
from django.contrib.auth import get_user_model;
get_user_model().MyUserMgr.create_user('mypass','AD','Joe','Smith','1233','joe#smith.com')
This is the line that's giving me trouble:
def save(self):
return self.MyUserMgr.upsertUser(self)
Right now, it works fine. But when I subclass BaseUserManager, I can't get it to work. What am I doing wrong? How should I restructure the code/references to properly use the included manager classes?
I've read all the relevant posts. I'm guessing the answer is in there somewhere, but it's all a jumbled mess to me at this point.
I am using:
Django 1.11
Python 2.7
Postgres 9.6
The error is caused by you trying to access the model manager from the instance.
In save() you're dealing with an instance of the model to be saved, so you can't access the manager. self is an instance (object) of the class, not the class itself.
First of all, I'd swap to the standard django approach with your manager which would be objects = MyUserMgr() so then you can do MyUserModel.objects.all(), MyUserModel.objects.upsertUser() etc.
Normally in Django you'd use a model manager to run queries that you want to use a lot so that you don't have to duplicate them in your views/forms etc.
Then you can just stick to saving the instance in the model save() method to start to try to simplify what you're doing because you've got quite complex already.
Have a look at the docs for Managers; https://docs.djangoproject.com/en/1.11/topics/db/managers/#managers
Then have a look at this really simple approach to extending the user model https://simpleisbetterthancomplex.com/tutorial/2016/07/22/how-to-extend-django-user-model.html

Django: How do i filter to return only those candidates that have paid >=10000

Here is my model.py
class Candidate(models.Model):
person = models.OneToOneField(
Person, related_name='person_candidate', on_delete=models.PROTECT)
def __str__(self):
return str(self.person)
#property
def total_candidate_votes(self):
return self.candidate_votes.filter(candidate=self).count()
#property
def amount_paid(self):
return self.candidate_payments.aggregate(models.Sum('fees'))['fees__sum'] or 0
#property
def is_qualified_to_vie(self):
return self.amount_paid >= 10000
Help me create a filter that will show candidates who have only paid >=10000
Filter.py
class CandidateFilter(django_filters.FilterSet):
name = django_filters.CharFilter(lookup_expr='iexact', name='person__first_name')
is_qualified_to_vie = django_filters.BooleanFilter(method='filter_by_qualified_candidates')
def filter_by_qualified_candidates(self, queryset, field, value):
return queryset.filter
The problem is that python properties can't be translated into django filter expressions. I'd recommend using custom queryset methods in addition to the python properties here. Something like the below:
class CandidateQuerySet(models.QuerySet):
def annotate_amount_paid(self):
return self.annotate(amount_paid=models.Sum('candidate_payments__fees'))
def qualified_to_vie(self, yes=True):
# eg, Candidate.objects.qualified_to_vie()
qs = return self.annotate_amount_paid()
if yes:
return qs.filter(amount_paid__gte=10000)
return qs.filter(amount_paid__lt=10000)
class Candidate(models.Model):
...
objects = CandidateQuerySet.as_manager()
From here, it's fairly straightforward.
class CandidateFilter(filters.FilterSet):
is_qualified_to_vie = django_filters.BooleanFilter(method='filter_by_qualified_candidates')
def filter_by_qualified_candidates(self, queryset, name, value):
return queryset.qualified_to_vie(value)
Note that the above is just the gist of the idea and will probably require some changes in order to actually function.

Flask-SQLAlchemy loading data but still responding with 500 [duplicate]

This question already has answers here:
How to serialize SqlAlchemy result to JSON?
(37 answers)
Closed 4 years ago.
I'm trying to jsonify a SQLAlchemy result set in Flask/Python.
The Flask mailing list suggested the following method http://librelist.com/browser//flask/2011/2/16/jsonify-sqlalchemy-pagination-collection-result/#04a0754b63387f87e59dda564bde426e :
return jsonify(json_list = qryresult)
However I'm getting the following error back:
TypeError: <flaskext.sqlalchemy.BaseQuery object at 0x102c2df90>
is not JSON serializable
What am I overlooking here?
I have found this question: How to serialize SqlAlchemy result to JSON? which seems very similar however I didn't know whether Flask had some magic to make it easier as the mailing list post suggested.
Edit: for clarification, this is what my model looks like
class Rating(db.Model):
__tablename__ = 'rating'
id = db.Column(db.Integer, primary_key=True)
fullurl = db.Column(db.String())
url = db.Column(db.String())
comments = db.Column(db.Text)
overall = db.Column(db.Integer)
shipping = db.Column(db.Integer)
cost = db.Column(db.Integer)
honesty = db.Column(db.Integer)
communication = db.Column(db.Integer)
name = db.Column(db.String())
ipaddr = db.Column(db.String())
date = db.Column(db.String())
def __init__(self, fullurl, url, comments, overall, shipping, cost, honesty, communication, name, ipaddr, date):
self.fullurl = fullurl
self.url = url
self.comments = comments
self.overall = overall
self.shipping = shipping
self.cost = cost
self.honesty = honesty
self.communication = communication
self.name = name
self.ipaddr = ipaddr
self.date = date
It seems that you actually haven't executed your query. Try following:
return jsonify(json_list = qryresult.all())
[Edit]: Problem with jsonify is, that usually the objects cannot be jsonified automatically. Even Python's datetime fails ;)
What I have done in the past, is adding an extra property (like serialize) to classes that need to be serialized.
def dump_datetime(value):
"""Deserialize datetime object into string form for JSON processing."""
if value is None:
return None
return [value.strftime("%Y-%m-%d"), value.strftime("%H:%M:%S")]
class Foo(db.Model):
# ... SQLAlchemy defs here..
def __init__(self, ...):
# self.foo = ...
pass
#property
def serialize(self):
"""Return object data in easily serializable format"""
return {
'id' : self.id,
'modified_at': dump_datetime(self.modified_at),
# This is an example how to deal with Many2Many relations
'many2many' : self.serialize_many2many
}
#property
def serialize_many2many(self):
"""
Return object's relations in easily serializable format.
NB! Calls many2many's serialize property.
"""
return [ item.serialize for item in self.many2many]
And now for views I can just do:
return jsonify(json_list=[i.serialize for i in qryresult.all()])
[Edit 2019]:
In case you have more complex objects or circular references, use a library like marshmallow).
Here's what's usually sufficient for me:
I create a serialization mixin which I use with my models. The serialization function basically fetches whatever attributes the SQLAlchemy inspector exposes and puts it in a dict.
from sqlalchemy.inspection import inspect
class Serializer(object):
def serialize(self):
return {c: getattr(self, c) for c in inspect(self).attrs.keys()}
#staticmethod
def serialize_list(l):
return [m.serialize() for m in l]
All that's needed now is to extend the SQLAlchemy model with the Serializer mixin class.
If there are fields you do not wish to expose, or that need special formatting, simply override the serialize() function in the model subclass.
class User(db.Model, Serializer):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String)
password = db.Column(db.String)
# ...
def serialize(self):
d = Serializer.serialize(self)
del d['password']
return d
In your controllers, all you have to do is to call the serialize() function (or serialize_list(l) if the query results in a list) on the results:
def get_user(id):
user = User.query.get(id)
return json.dumps(user.serialize())
def get_users():
users = User.query.all()
return json.dumps(User.serialize_list(users))
I had the same need, to serialize into json. Take a look at this question. It shows how to discover columns programmatically. So, from that I created the code below. It works for me, and I'll be using it in my web app. Happy coding!
def to_json(inst, cls):
"""
Jsonify the sql alchemy query result.
"""
convert = dict()
# add your coversions for things like datetime's
# and what-not that aren't serializable.
d = dict()
for c in cls.__table__.columns:
v = getattr(inst, c.name)
if c.type in convert.keys() and v is not None:
try:
d[c.name] = convert[c.type](v)
except:
d[c.name] = "Error: Failed to covert using ", str(convert[c.type])
elif v is None:
d[c.name] = str()
else:
d[c.name] = v
return json.dumps(d)
class Person(base):
__tablename__ = 'person'
id = Column(Integer, Sequence('person_id_seq'), primary_key=True)
first_name = Column(Text)
last_name = Column(Text)
email = Column(Text)
#property
def json(self):
return to_json(self, self.__class__)
Here's my approach:
https://github.com/n0nSmoker/SQLAlchemy-serializer
pip install SQLAlchemy-serializer
You can easily add mixin to your model and then just call
.to_dict() method on its instance.
You also can write your own mixin on base of SerializerMixin.
For a flat query (no joins) you can do this
#app.route('/results/')
def results():
data = Table.query.all()
result = [d.__dict__ for d in data]
return jsonify(result=result)
and if you only want to return certain columns from the database you can do this
#app.route('/results/')
def results():
cols = ['id', 'url', 'shipping']
data = Table.query.all()
result = [{col: getattr(d, col) for col in cols} for d in data]
return jsonify(result=result)
Ok, I've been working on this for a few hours, and I've developed what I believe to be the most pythonic solution yet. The following code snippets are python3 but shouldn't be too horribly painful to backport if you need.
The first thing we're gonna do is start with a mixin that makes your db models act kinda like dicts:
from sqlalchemy.inspection import inspect
class ModelMixin:
"""Provide dict-like interface to db.Model subclasses."""
def __getitem__(self, key):
"""Expose object attributes like dict values."""
return getattr(self, key)
def keys(self):
"""Identify what db columns we have."""
return inspect(self).attrs.keys()
Now we're going to define our model, inheriting the mixin:
class MyModel(db.Model, ModelMixin):
id = db.Column(db.Integer, primary_key=True)
foo = db.Column(...)
bar = db.Column(...)
# etc ...
That's all it takes to be able to pass an instance of MyModel() to dict() and get a real live dict instance out of it, which gets us quite a long way towards making jsonify() understand it. Next, we need to extend JSONEncoder to get us the rest of the way:
from flask.json import JSONEncoder
from contextlib import suppress
class MyJSONEncoder(JSONEncoder):
def default(self, obj):
# Optional: convert datetime objects to ISO format
with suppress(AttributeError):
return obj.isoformat()
return dict(obj)
app.json_encoder = MyJSONEncoder
Bonus points: if your model contains computed fields (that is, you want your JSON output to contain fields that aren't actually stored in the database), that's easy too. Just define your computed fields as #propertys, and extend the keys() method like so:
class MyModel(db.Model, ModelMixin):
id = db.Column(db.Integer, primary_key=True)
foo = db.Column(...)
bar = db.Column(...)
#property
def computed_field(self):
return 'this value did not come from the db'
def keys(self):
return super().keys() + ['computed_field']
Now it's trivial to jsonify:
#app.route('/whatever', methods=['GET'])
def whatever():
return jsonify(dict(results=MyModel.query.all()))
If you are using flask-restful you can use marshal:
from flask.ext.restful import Resource, fields, marshal
topic_fields = {
'title': fields.String,
'content': fields.String,
'uri': fields.Url('topic'),
'creator': fields.String,
'created': fields.DateTime(dt_format='rfc822')
}
class TopicListApi(Resource):
def get(self):
return {'topics': [marshal(topic, topic_fields) for topic in DbTopic.query.all()]}
You need to explicitly list what you are returning and what type it is, which I prefer anyway for an api. Serialization is easily taken care of (no need for jsonify), dates are also not a problem. Note that the content for the uri field is automatically generated based on the topic endpoint and the id.
Here's my answer if you're using the declarative base (with help from some of the answers already posted):
# in your models definition where you define and extend declarative_base()
from sqlalchemy.ext.declarative import declarative_base
...
Base = declarative_base()
Base.query = db_session.query_property()
...
# define a new class (call "Model" or whatever) with an as_dict() method defined
class Model():
def as_dict(self):
return { c.name: getattr(self, c.name) for c in self.__table__.columns }
# and extend both the Base and Model class in your model definition, e.g.
class Rating(Base, Model):
____tablename__ = 'rating'
id = db.Column(db.Integer, primary_key=True)
fullurl = db.Column(db.String())
url = db.Column(db.String())
comments = db.Column(db.Text)
...
# then after you query and have a resultset (rs) of ratings
rs = Rating.query.all()
# you can jsonify it with
s = json.dumps([r.as_dict() for r in rs], default=alchemyencoder)
print (s)
# or if you have a single row
r = Rating.query.first()
# you can jsonify it with
s = json.dumps(r.as_dict(), default=alchemyencoder)
# you will need this alchemyencoder where your are calling json.dumps to handle datetime and decimal format
# credit to Joonas # http://codeandlife.com/2014/12/07/sqlalchemy-results-to-json-the-easy-way/
def alchemyencoder(obj):
"""JSON encoder function for SQLAlchemy special classes."""
if isinstance(obj, datetime.date):
return obj.isoformat()
elif isinstance(obj, decimal.Decimal):
return float(obj)
Flask-Restful 0.3.6 the Request Parsing recommend marshmallow
marshmallow is an ORM/ODM/framework-agnostic library for converting
complex datatypes, such as objects, to and from native Python
datatypes.
A simple marshmallow example is showing below.
from marshmallow import Schema, fields
class UserSchema(Schema):
name = fields.Str()
email = fields.Email()
created_at = fields.DateTime()
from marshmallow import pprint
user = User(name="Monty", email="monty#python.org")
schema = UserSchema()
result = schema.dump(user)
pprint(result)
# {"name": "Monty",
# "email": "monty#python.org",
# "created_at": "2014-08-17T14:54:16.049594+00:00"}
The core features contain
Declaring Schemas
Serializing Objects (“Dumping”)
Deserializing Objects (“Loading”)
Handling Collections of Objects
Validation
Specifying Attribute Names
Specifying Serialization/Deserialization Keys
Refactoring: Implicit Field Creation
Ordering Output
“Read-only” and “Write-only” Fields
Specify Default Serialization/Deserialization Values
Nesting Schemas
Custom Fields
Here is a way to add an as_dict() method on every class, as well as any other method you want to have on every single class.
Not sure if this is the desired way or not, but it works...
class Base(object):
def as_dict(self):
return dict((c.name,
getattr(self, c.name))
for c in self.__table__.columns)
Base = declarative_base(cls=Base)
I've been looking at this problem for the better part of a day, and here's what I've come up with (credit to https://stackoverflow.com/a/5249214/196358 for pointing me in this direction).
(Note: I'm using flask-sqlalchemy, so my model declaration format is a bit different from straight sqlalchemy).
In my models.py file:
import json
class Serializer(object):
__public__ = None
"Must be implemented by implementors"
def to_serializable_dict(self):
dict = {}
for public_key in self.__public__:
value = getattr(self, public_key)
if value:
dict[public_key] = value
return dict
class SWEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Serializer):
return obj.to_serializable_dict()
if isinstance(obj, (datetime)):
return obj.isoformat()
return json.JSONEncoder.default(self, obj)
def SWJsonify(*args, **kwargs):
return current_app.response_class(json.dumps(dict(*args, **kwargs), cls=SWEncoder, indent=None if request.is_xhr else 2), mimetype='application/json')
# stolen from https://github.com/mitsuhiko/flask/blob/master/flask/helpers.py
and all my model objects look like this:
class User(db.Model, Serializer):
__public__ = ['id','username']
... field definitions ...
In my views I call SWJsonify wherever I would have called Jsonify, like so:
#app.route('/posts')
def posts():
posts = Post.query.limit(PER_PAGE).all()
return SWJsonify({'posts':posts })
Seems to work pretty well. Even on relationships. I haven't gotten far with it, so YMMV, but so far it feels pretty "right" to me.
Suggestions welcome.
I was looking for something like the rails approach used in ActiveRecord to_json and implemented something similar using this Mixin after being unsatisfied with other suggestions. It handles nested models, and including or excluding attributes of the top level or nested models.
class Serializer(object):
def serialize(self, include={}, exclude=[], only=[]):
serialized = {}
for key in inspect(self).attrs.keys():
to_be_serialized = True
value = getattr(self, key)
if key in exclude or (only and key not in only):
to_be_serialized = False
elif isinstance(value, BaseQuery):
to_be_serialized = False
if key in include:
to_be_serialized = True
nested_params = include.get(key, {})
value = [i.serialize(**nested_params) for i in value]
if to_be_serialized:
serialized[key] = value
return serialized
Then, to get the BaseQuery serializable I extended BaseQuery
class SerializableBaseQuery(BaseQuery):
def serialize(self, include={}, exclude=[], only=[]):
return [m.serialize(include, exclude, only) for m in self]
For the following models
class ContactInfo(db.Model, Serializer):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
full_name = db.Column(db.String())
source = db.Column(db.String())
source_id = db.Column(db.String())
email_addresses = db.relationship('EmailAddress', backref='contact_info', lazy='dynamic')
phone_numbers = db.relationship('PhoneNumber', backref='contact_info', lazy='dynamic')
class EmailAddress(db.Model, Serializer):
id = db.Column(db.Integer, primary_key=True)
email_address = db.Column(db.String())
type = db.Column(db.String())
contact_info_id = db.Column(db.Integer, db.ForeignKey('contact_info.id'))
class PhoneNumber(db.Model, Serializer):
id = db.Column(db.Integer, primary_key=True)
phone_number = db.Column(db.String())
type = db.Column(db.String())
contact_info_id = db.Column(db.Integer, db.ForeignKey('contact_info.id'))
phone_numbers = db.relationship('Invite', backref='phone_number', lazy='dynamic')
You could do something like
#app.route("/contact/search", methods=['GET'])
def contact_search():
contact_name = request.args.get("name")
matching_contacts = ContactInfo.query.filter(ContactInfo.full_name.like("%{}%".format(contact_name)))
serialized_contact_info = matching_contacts.serialize(
include={
"phone_numbers" : {
"exclude" : ["contact_info", "contact_info_id"]
},
"email_addresses" : {
"exclude" : ["contact_info", "contact_info_id"]
}
}
)
return jsonify(serialized_contact_info)
I was working with a sql query defaultdict of lists of RowProxy objects named jobDict
It took me a while to figure out what Type the objects were.
This was a really simple quick way to resolve to some clean jsonEncoding just by typecasting the row to a list and by initially defining the dict with a value of list.
jobDict = defaultdict(list)
def set_default(obj):
# trickyness needed here via import to know type
if isinstance(obj, RowProxy):
return list(obj)
raise TypeError
jsonEncoded = json.dumps(jobDict, default=set_default)
I just want to add my method to do this.
just define a custome json encoder to serilize your db models.
class ParentEncoder(json.JSONEncoder):
def default(self, obj):
# convert object to a dict
d = {}
if isinstance(obj, Parent):
return {"id": obj.id, "name": obj.name, 'children': list(obj.child)}
if isinstance(obj, Child):
return {"id": obj.id, "name": obj.name}
d.update(obj.__dict__)
return d
then in your view function
parents = Parent.query.all()
dat = json.dumps({"data": parents}, cls=ParentEncoder)
resp = Response(response=dat, status=200, mimetype="application/json")
return (resp)
it works well though the parent have relationships
It's been a lot of times and there are lots of valid answers, but the following code block seems to work:
my_object = SqlAlchemyModel()
my_serializable_obj = my_object.__dict__
del my_serializable_obj["_sa_instance_state"]
print(jsonify(my_serializable_object))
I'm aware that this is not a perfect solution, nor as elegant as the others, however for those who want o quick fix, they might try this.

Does Django ORM have an equivalent to SQLAlchemy's Hybrid Attribute?

In SQLAlchemy, a hybrid attribute is either a property or method applied to an ORM-mapped class,
class Interval(Base):
__tablename__ = 'interval'
id = Column(Integer, primary_key=True)
start = Column(Integer, nullable=False)
end = Column(Integer, nullable=False)
def __init__(self, start, end):
self.start = start
self.end = end
#hybrid_property
def length(self):
return self.end - self.start
#hybrid_method
def contains(self,point):
return (self.start <= point) & (point < self.end)
#hybrid_method
def intersects(self, other):
return self.contains(other.start) | self.contains(other.end)
This allows for distinct behaviors at the class and instance levels, thus making it simpler to evaluate SQL statements using the same code,
>>> i1 = Interval(5, 10)
>>> i1.length
5
>>> print Session().query(Interval).filter(Interval.length > 10)
SELECT interval.id AS interval_id, interval.start AS interval_start,
interval."end" AS interval_end
FROM interval
WHERE interval."end" - interval.start > :param_1
Now in Django, if I have a property on a model,
class Person(models.Model):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
def _get_full_name(self):
"Returns the person's full name."
return '%s %s' % (self.first_name, self.last_name)
full_name = property(_get_full_name)
It is my understanding that I can not do the following,
Person.objects.filter(full_name="John Cadengo")
Is there an equivalent of SQLAlchemy's hybrid attribute in Django? If not, is there perhaps a common workaround?
You're right that you cannot apply django queryset filter based on python properties, because filter operates on a database level. It seems that there's no equivalent of SQLAlchemy's hybrid attributes in Django.
Please, take a look here and here, may be it'll help you to find a workaround. But, I think there is no generic solution.
One of the quick possible workarounds is to implement some descriptor, that would apply expressions via the annotation.
Something like this:
from django.db import models
from django.db.models import functions
class hybrid_property:
def __init__(self, func):
self.func = func
self.name = func.__name__
self.exp = None
def __get__(self, instance, owner):
if instance is None:
return self
return self.func(instance)
def __set__(self, instance, value):
pass
def expression(self, exp):
self.exp = exp
return self
class HybridManager(models.Manager):
def get_queryset(self):
qs = super().get_queryset()
for name, value in vars(qs.model).items():
if isinstance(value, hybrid_property) and value.exp is not None:
qs = qs.annotate(**{name: value.exp(qs.model)})
return qs
class TestAttribute(models.Model):
val1 = models.CharField(max_length=256)
val2 = models.CharField(max_length=256)
objects = HybridManager()
#hybrid_property
def vals(self):
return f"{self.val1} {self.val2}"
#vals.expression
def vals(cls):
return functions.Concat(models.F("val1"), models.Value(" "), models.F("val2"))
class HybridTests(TestCase):
def setUp(self) -> None:
self.test_attr = TestAttribute.objects.create(val1="val1", val2="val2")
def test_access(self):
self.assertTrue(TestAttribute.objects.exists())
self.assertEqual(self.test_attr.vals, f"{self.test_attr.val1} {self.test_attr.val2}")
self.assertTrue(TestAttribute.objects.filter(vals=f"{self.test_attr.val1} {self.test_attr.val2}").exists())
self.assertTrue(TestAttribute.objects.filter(vals__iexact=f"{self.test_attr.val1} {self.test_attr.val2}").exists())

Categories