Refactoring a flask-restful api endpoint into separate linked resources - sqlalchemy

I'm re-factoring some code on an API and I'm unsure how to structure the related Models in Flask-Restful and SQLAlchemy. Partial solution half-way down the page.
What I'm essentially attempting to do is the the equivalent of this SQL
SELECT tags.name
FROM events, tags, events_tags
WHERE events.id = events_tags.event_id
AND tags.id = events_tags.tag_id
AND events.id = 1
My routes originally had an event with a nested set of tags and the JSON returned by api.add_resource(Events, '/events/<int:event_id>') was structured something like this:
{
"event_id":"1",
"title":"Sample title",
"tags":[
{"name":"earthquake"},
{"name":"infrastructure"}
]
}
It's the tags which I want to separate out into a separate endpoint:
So that we end up with two JSON outputs
api.add_resource(Event, '/events/<int:event_id>')
[
{
"event_id":"1",
"title":"Sample title",
}
]
api.add_resource(Tags, '/events/<int:event_id>/tags')
[
{"name":"earthquake"},
{"name":"infrastructure"}
]
As I'm working through the problem, and being aware that any given tag might belong to multiple events I'm thinking that the solution would need the following done:
An event / tag joining table in the DB.
A function for a tag_association_table in the api
A TagsListModel (but I'm unsure as to whether this references this tag_association_table or not.
Any help is welcome.
TAG MODEL
class TagsListModel(db.Model):
__tablename__ = "tags"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, nullable=False)
events = db.relationship("EventModel", secondary=tag_association_table, backref="tag")
TAG SERIALISER
class TagSchema(SQLAlchemyAutoSchema):
class Meta:
model = TagModel
load_instance = True
include_fk = True
id = auto_field(load_only=True)
TAG LOADER
class Tags(Resource):
# GET
def get(self, event_id):
schema = TagsListSchema()
result = db.session.query(TagsListModel).filter(TagsListModel.event_id == event_id)
return schema.dump(result, many=True), 200
# POST
def post(self, event_id):
event_id = event_id
name = request.json['name']
tag = TagsListModel(name=name)
db.session.add(tag)
db.session.commit()
data = ({'id' :tag.id,
'name': tag.name
})
#print(data)
response = jsonify(data)
response.status_code = 200 # or 400 or whatever
return response
ROUTES
# Route_1
## GET (RETURNS) A LIST OF EVENTS
## POST (CREATES) AN EVENT
api.add_resource(Events, '/events')
# Route_2
## GET (RETURNS) A SINGLE EVENT
# PUTS (UPDATES) A SINGLE EVENT
api.add_resource(Events, '/events/<int:event_id>')
# Route_3
## GET (RETURNS) ALL TAGS FOR AN EVENT
api.add_resource(Tags, '/events/<int:event_id>/tags')
Partial Solution
I have been able to retrieve the values on a joining table. These are correctly filtered and provides the following output:
[
{
"tag_id": 1,
"event_id": 1
},
{
"tag_id": 2,
"event_id": 1
}
]
Endpoint
I call the below endpoint with the int:event_id value of 1:
# Route_3
## GET (RETURNS) ALL TAGS FOR AN EVENT
api.add_resource(EventTags, '/events/<int:event_id>/tags')
Loader
class EventTags(Resource):
# GET
def get(self, event_id):
schema = TagSchema()
result = db.session.query(TagModel).filter(TagModel.event_id == event_id)
return schema.dump(result, many=True), 200
# POST
def post(self, event_id):
event_id = event_id
tag_id = request.json['id']
tag = TagsListModel(id=id)
db.session.add(tag)
db.session.commit()
data = ({'id' :tag.id
})
#print(data)
response = jsonify(data)
response.status_code = 200 # or 400 or whatever
return response
Schema
class TagSchema(SQLAlchemyAutoSchema):
class Meta:
model = TagModel
load_instance = True
include_fk = True
tag_id = auto_field(load_only=False)
event_id = auto_field(load_only=False)
Model
class TagModel(db.Model):
__tablename__ = "events_tags"
tag_id = Column(Integer, primary_key=True)
event_id = Column(Integer, primary_key=True)

Fixed it.
I had to:
Create a model for each table where it didn't exist: (EventTag, Tag)
Modify my tag schema to match the values I wanted to hide from the Tag table
Modify my EventTags Loader to filter on the columns across the three tables
MODELS
class TagModel(db.Model):
__tablename__ = "tags"
id = Column(Integer, primary_key=True)
name = Column(db.String, nullable=False)
class EventTagModel(db.Model):
__tablename__ = "events_tags"
tag_id = Column(Integer, primary_key=True)
event_id = Column(Integer, primary_key=True)
SCHEMA
class TagSchema(SQLAlchemyAutoSchema):
class Meta:
model = TagModel
load_instance = True
include_fk = True
id = auto_field(load_only=True) # Only one field removed so this remained pretty identical.
LOADER
result = db.session.query(TagModel)
.filter(TagModel.id == EventTagModel.tag_id)
.filter(EventModel.id == EventTagModel.event_id)
.filter(EventTagModel.event_id == event_id)
# result = db.session.query(TagModel). # REMOVED
# filter(TagModel.event_id == event_id). # REMOVED

Related

In SQLAlchemy, how should I specify that the relationship field is required?

I have a model that depends on some fields on another model. This fields should be present when the record is created, but I do not see a way to enforce that on the database:
class Study(db.Model):
id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
type = db.Column(Enum(StudyTypeChoices), nullable=False)
owner_id = db.Column(UUID(as_uuid=True), db.ForeignKey('owner.id'), nullable=False)
participants = db.relationship('Participant', lazy=True, cascade='save-update, merge, delete')
How can I make sure that 'participants' is provided when the Study record gets created (similar to what happens with the 'type' field)? I know I can put a wrapper around it to make sure of that, but I am wondering is there is a more neat way of doing it with sqlalchemy.
Edit: This is the definition of the Participant model
class Participant(UserBase):
id = db.Column(UUID(as_uuid=True), db.ForeignKey("user_base.id"), primary_key=True)
study_id = db.Column(UUID(as_uuid=True), db.ForeignKey('study.id'))
You can listen to before_flush events and prevent flushes containing studies without participants by raising an exception for instance.
#event.listens_for(Session, "before_flush")
def before_flush(session, flush_context, instances):
for instance in session.new: # might want to inspect session.dirty as well
if isinstance(instance, Study) and (
instance.participants is None or instance.participants == []
):
raise ValueError(
f"Study {instance} cannot have {instance.participants} participants."
)
This only checks for new studies, you might want to check in session.dirty as well for updated studies.
Full demo:
from sqlalchemy import Column, ForeignKey, Integer, create_engine, event
from sqlalchemy.orm import Session, declarative_base, relationship
Base = declarative_base()
class Study(Base):
__tablename__ = "study"
id = Column(Integer, primary_key=True)
participants = relationship("Participant", uselist=True, back_populates="study")
class Participant(Base):
__tablename__ = "participant"
id = Column(Integer, primary_key=True)
study_id = Column(Integer, ForeignKey("study.id"), nullable=True)
study = relationship("Study", back_populates="participants")
#event.listens_for(Session, "before_flush")
def before_flush(session, flush_context, instances):
for instance in session.new: # might want to inspect session.dirty as well
if isinstance(instance, Study) and (
instance.participants is None or instance.participants == []
):
raise ValueError(
f"Study {instance} cannot have {instance.participants} participants."
)
engine = create_engine("sqlite://", future=True, echo=True)
Base.metadata.create_all(engine)
s1 = Study()
p1_1 = Participant()
p1_2 = Participant()
s1.participants.extend([p1_1, p1_2])
s2 = Study()
with Session(bind=engine) as session:
session.add(s1)
session.commit() # OK
with Session(bind=engine) as session:
session.add(s2)
session.commit() # ValueError

Update multiple model data through one serializer

Please go through the description, I tried to describe everything i've encountered while trying to solve this issue.
I have two models, User and DoctorProfile. User model has OneToOne relation with DoctorProfile. I'm trying to update data of both model through one serializer. I've combined two models into one serilzer like below:
class DoctorProfileFields(serializers.ModelSerializer):
"""this will be used as value of profile key in DoctorProfileSerializer"""
class Meta:
model = DoctorProfile
fields = ('doctor_type', 'title', 'date_of_birth', 'registration_number', 'gender', 'city', 'country', )
class DoctorProfileSerializer(serializers.ModelSerializer):
"""retrieve, update and delete profile"""
profile = DoctorProfileFields(source='*')
class Meta:
model = User
fields = ('name', 'avatar', 'profile', )
#transaction.atomic
def update(self, instance, validated_data):
ModelClass = self.Meta.model
profile = validated_data.pop('profile', {})
ModelClass.objects.filter(id=instance.id).update(**validated_data)
if profile:
DoctorProfile.objects.filter(owner=instance).update(**profile)
new_instance = ModelClass.objects.get(id = instance.id)
return new_instance
When I send request with GET method, the DoctorProfileSerializer returns nested data(Combining two models User and DoctorProfile) in the desired fashion.
But when I try to update both models through this serializer, it returns error saying User has no field named 'doctor_type'.
Let's have a look at the JSON i'm trying to send:
{
"name": "Dr. Strange updated twice",
"profile" : {
"doctor_type": "PSYCHIATRIST"
}
}
Let's have a look at how the serializer is receiving the JSON:
{
"name": "Maruf updated trice",
"doctor_type": "PSYCHIATRIST"
}
Models:
class CustomUser(AbstractBaseUser, PermissionsMixin):
class Types(models.TextChoices):
DOCTOR = "DOCTOR", "Doctor"
PATIENT = "PATIENT", "Patient"
#Type of user
type = models.CharField(_("Type"), max_length=50, choices=Types.choices, null=True, blank=False)
avatar = models.ImageField(upload_to="avatars/", null=True, blank=True)
email = models.EmailField(max_length=255, unique=True)
name = models.CharField(max_length=255)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
objects = CustomBaseUserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['name', 'type'] #email is required by default
def get_full_name(self):
return self.name
def __str__(self):
return self.email
class DoctorProfile(models.Model):
"""Model for Doctors profile"""
class DoctorType(models.TextChoices):
"""Doctor will choose profession category from enum"""
PSYCHIATRIST = "PSYCHIATRIST", "Psychiatrist"
PSYCHOLOGIST = "PSYCHOLOGIST", "Psychologist"
DERMATOLOGIST = "DERMATOLOGIST", "Dermatologist"
SEXUAL_HEALTH = "SEXUAL HEALTH", "Sexual health"
GYNECOLOGIST = "GYNECOLOGIST", "Gynecologist"
INTERNAL_MEDICINE = "INTERNAL MEDICINE", "Internal medicine"
DEVELOPMENTAL_THERAPIST = "DEVELOPMENTAL THERAPIST", "Developmental therapist"
owner = models.OneToOneField(
CustomUser,
on_delete=models.CASCADE,
related_name='doctor_profile'
)
doctor_type = models.CharField(
_("Profession Type"),
max_length=70,
choices=DoctorType.choices,
null=True,
blank=False
)
title = models.IntegerField(_('Title'), default=1, choices=TITLES)
date_of_birth = models.DateField(null=True, blank=False)
gender = models.IntegerField(_('Gender'), default=1, choices=GENDERS)
registration_number = models.IntegerField(_('Registration Number'), null=True, blank=False)
city = models.CharField(_('City'), max_length=255, null=True, blank=True)
country = models.CharField(_('Country'), max_length=255, null=True, blank=True)
def __str__(self):
return f'profile-{self.id}-{self.title} {self.owner.get_full_name()}'
How do I know that the serializer is getting wrong JSON? I debugged the validated_data in the DoctorProfileSerializer and it's showing that it's a flat JSON, there's no key named profile.
I'm assuming the problem is with the source that I've added in the DoctorProfileSerializer. But if I don't use the source the get method returns the following error
Got AttributeError when attempting to get a value for field profile on serializer (DoctorProfileSerializer).
Please let me know if it's solvable also if it's a good approach to do it this way?
Ok, sorry if my answer is too long but let me try to answer step by step,
Models:
class DoctorProfile(models.Model):
# everything as it is
# except I feel comfortable using ForeignKey :D
owner = models.ForeignKey(
CustomUser,
on_delete=models.CASCADE,
related_name='doctor_profile'
)
# everything as it is
class CustomUser(AbstractBaseUser, PermissionsMixin):
# as it is
Serializers:
class DoctorProfileSerializer(serializers.ModelSerializer):
"""Serializer for DoctorProfile."""
class Meta(object):
model = DoctorProfile
fields = [
'id',
'doctor_type',
'title',
'date_of_birth',
'registration_number',
'gender',
'city',
'country',
]
read_only_fields = [
'id',
]
class CustomUserSerializer(serializers.ModelSerializer):
"""Serializer for DoctorProfile."""
# here I'm renaming the related object exactly as the
# related name you've provided on model
doctor_profile = DoctorProfileSerializer(many=False)
class Meta(object):
model = CustomUser
fields = [
'name',
'avatar',
'doctor_profile',
]
read_only_fields = [
'id',
]
def update(self, instance, validated_data):
# instance is the current row of CustomUser
# validated_data is the new incoming data
# use validated_data.pop('doctor_profile') to extract
# doctor_profile data and do whatever is needed on
# DoctorProfile model
# compare them and perform your update method
# as you wish on the DoctorProfile model
# object after updating models, you can query the total
# object again before returning if you want
return updated_object
View:
class CustomUserAPIView(RetrieveUpdateAPIView):
"""CustomUserAPIView."""
permission_classes = [IsAuthenticated]
model = CustomUser
serializer_class = CustomUserSerializer
lookup_field = 'id'
#for returning logged in user info only
def get_queryset(self):
return CustomUser.objects.filter(id=self.request.user.id).first()
def update(self, request, *args, **kwargs):
"""Update override."""
partial = kwargs.pop('partial', False)
instance = self.get_object()
serializer = self.get_serializer(
instance,
data=request.data,
partial=partial,
)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
custom_user_obj = CustomUser.objects.filter(
id=instance.id,
).first()
serializer = CustomUserSerializer(custom_user_obj)
return Response(serializer.data)
Run the migration and let me know if you are getting the expected output on GET method. For UPDATE method if you face any problem let me know I will update the answer accordingly right away.
For keeping all the Django Rest Framework related docs handy, use this link
https://www.cdrf.co/

DRF Create queryset by adding foreign key queryset into it

I want to get foreign data(Child model) into Parent Model queryset.
I am able to add foreign data into Parent data but it is JSON. I want to convert that JSON into queryset because get_queryset() returns model/queryset.
I googled a lot but unable to find anything helpful.
class Parent(models.Model):
parent_name = models.TextField()
child = models.ForeignKey(Child, related_name="+", on_delete=models.CASCADE, null=True)
class Child(models.Model):
child_name = models.TextField()
class ParentViewSet(viewsets.ModelViewSet):
queryset = Parent.objects.all()
serializer_class = ParentInfoSerializer
filter_backends = (filters.DjangoFilterBackend,)
filter_fields = ['parent_name']
def get_queryset(self):
response = []
parent_name = self.request.GET.getlist("parent_name")[0]#
parent_queryset =
Parent.objects.filter(parent_name=parent_name)
for par_queryset in parent_queryset:
parent_result = self.serializer_class(phy_queryset).data
child_id = physician_info_result["child"]
child_instance = Child.objects.get(pk=child_id)
child_result = ChildSerializer(child_instance).data
parent_result["child"] = child_result
response.append(parent_result)
return response
URL -
http://localhost:9000/api/parent?parent_name=xyz
Response output is:
[{
"parent_name": "xyz",
"child": [{
"id": 1
"child_name": "abc"
}]
}]
But above output it is JSON which I don't want. I want output into queryset/model.
NOTE: Queryset output should contain foreign queryset.
You could just replace the default field for child to child serializer and it'll work like you want.
class ParentSerializer(models.Model):
child = ChildSerializer()
class Meta:
models = Parent
fields = ('parent_name', 'child')
And you also don't need to override the get_queryset method.

SQLAlchemy: foreign keys to declared_attr columns

I'm having trouble using the foreign_keys argument with declared_attr columns. My models look like this:
class BasicTable(object):
created = db.Column(db.DateTime)
last_modified = db.Column(db.DateTime)
#declared_attr
def created_by_id(cls):
return db.Column(db.Integer, db.ForeignKey("app_user.id", use_alter = True, name='fk_created_by_id'))
#declared_attr
def created_by(cls):
return db.relationship("AppUser", foreign_keys='{}.{}'.format(cls.__tablename__, 'created_by_id'))
#declared_attr
def last_modified_by_id(cls):
return db.Column(db.Integer, db.ForeignKey("app_user.id", use_alter = True, name='fk_last_modified_by_id'))
#declared_attr
def last_modified_by(cls):
return db.relationship("AppUser", foreign_keys='{}.{}'.format(cls.__tablename__, 'last_modified_by_id'))
class AppUser(BasicTable, db.Model):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(64))
service_id = db.Column(db.Integer, db.ForeignKey("service.id"))
Because there are two columns in BasicTable that reference AppUser, I was getting "ambiguous foreign keys" errors, so I tried to use the foreign_keys argument as described here. The above gives me this error:
AttributeError: 'Table' object has no attribute 'last_modified_by_id'
When I check the database, that field does exist on all the tables that use BasicTable. Is this error happening because I'm referencing a declared_attr column? This suggests so, but when I tried to use the lambda technique like this:
foreign_keys=lambda: cls.created_by_id
I get this error:
InvalidRequestError: When initializing mapper Mapper|AppUser|app_user, expression 'BasicTable' failed to locate a name ("name 'BasicTable' is not defined"). If this is a class name, consider adding this relationship() to the <class 'app.models.AppUser'> class after both dependent classes have been defined.
Is there a way around this? Thanks!
class BasicTable(object):
created = db.Column(db.DateTime)
last_modified = db.Column(db.DateTime)
#declared_attr
def created_by_id(cls):
return db.Column(db.Integer, db.ForeignKey("app_user.id", use_alter = True, name='fk_created_by_id'))
#declared_attr
def created_by(cls):
return db.relationship('AppUser', primaryjoin='%s.created_by_id==AppUser.id' % cls.__name__,
remote_side='AppUser.id')
#declared_attr
def last_modified_by_id(cls):
return db.Column(db.Integer, db.ForeignKey("app_user.id", use_alter = True, name='fk_last_modified_by_id'))
#declared_attr
def last_modified_by(cls):
return db.relationship('AppUser', primaryjoin='%s.last_modified_by_id==AppUser.id' % cls.__name__,
remote_side='AppUser.id')

Django Rest Framework: Deserializing and get the primary key from validated_data

I defined a nested model Product as follow. Each Product can belong to a lot of Productlist.
class Product(models.Model):
product_id = models.AutoField(primary_key=True)
product_name = models.CharField(max_length=50)
class Productlist(models.Model):
productlist_id = models.AutoField(primary_key=True)
productlist_name = models.CharField(max_length=50)
product = models.ManyToManyField(Product, related_name='productlists')
The corresponding serializers are:
class ProductlistSerializer(serializers.ModelSerializer):
class Meta:
model = Productlist
fields = ('productlist_id', 'productlist_name',)
class ProductSerializer(serializers.ModelSerializer):
productlists = ProductlistSerializer(many=True, required=False)
class Meta:
model = Product
fields = ('product_id', 'product_name', 'product lists')
def create(self, validated_data):
#some codes
When I POST a new Product (url(r'^api/products/$', views.ProductEnum.as_view()), I would like to update the product lists for adding the new product to the corresponding product lists. The JSON file I prefer to use is:
{
"product_name": "product1"
"productlist": [
{
"productlist_id": 1,
"productlist_name": "list1",
},
{
"productlist_id": 2,
"productlist_name": list2"
}
]
}
The problem is that I cannot get the productlist_id from validated_data. In Django Rest Framework, you always need to call to_internal_value() for deserializing data and generate validated_data. After some degugging, I checked the code of DRF and find the following snippets in to_internal_value():
def to_internal_value(self, data):
"""
Dict of native values <- Dict of primitive datatypes.
"""
if not isinstance(data, dict):
message = self.error_messages['invalid'].format(
datatype=type(data).__name__
)
raise ValidationError({
api_settings.NON_FIELD_ERRORS_KEY: [message]
})
ret = OrderedDict()
errors = OrderedDict()
fields = [
field for field in self.fields.values()
if (not field.read_only) or (field.default is not empty)
]
for field in fields:
validate_method = getattr(self, 'validate_' + field.field_name, None)
primitive_value = field.get_value(data)
try:
validated_value = field.run_validation(primitive_value)
if validate_method is not None:
validated_value = validate_method(validated_value)
except ValidationError as exc:
errors[field.field_name] = exc.detail
except DjangoValidationError as exc:
errors[field.field_name] = list(exc.messages)
except SkipField:
pass
else:
set_value(ret, field.source_attrs, validated_value)
if errors:
raise ValidationError(errors)
return ret
Please notice the to_internal_value's fields has ignored the IntegerField(read_only=True) for it cannot satisfy the following condition:
fields = [
field for field in self.fields.values()
if (not field.read_only) or (field.default is not empty)
]
So the validated_data will just have the following data:
{
"product_name": "product1"
"productlist": [
{
"productlist_name": "list1",
},
{
"productlist_name": list2"
}
]
}
How could I get the primary key of product list? Thanks in advance!
After some digging, I found that the read_only fields are only for output presentation. You can find the similar question on the offcial github link of Django REST Framework.
So the solution is overriding the read_only field in the serializer as follow:
class ProductlistSerializer(serializers.ModelSerializer):
productlist_id = serializers.IntegerField(read_only=False)
class Meta:
model = Productlist
fields = ('productlist_id', 'productlist_name',)