In FastAPI I have a situation similar to this:
models.py
class MachineGroups(Base):
__tablename__ = 'MachineGroups'
MachineGroupsId = Column(NVARCHAR(25), primary_key=True)
Description = Column(NVARCHAR(255))
class Machines(Base):
__tablename__ = 'Machines'
MachinesId = Column(NVARCHAR(50), primary_key=True)
Description = Column(NVARCHAR(255))
MachineGroupsId = Column(NVARCHAR(25), ForeignKey("MachineGroups.MachineGroupsId", ondelete="SET NULL"), nullable=True)
group = relationship("MachineGroups")
schemas.py
class MachineGroups(BaseModel):
MachineGroupsId: str
Description: str
class Config:
orm_mode = True
class Machines(BaseModel):
MachinesId: str
Description: str
MachineGroupsId: str = None
group: Optional[MachineGroups] = None
class Config:
orm_mode = True
In the controller I have a function create like this:
controller.py
#app.get(
"/machines",
response_model=List[machine_schemas.Machines],
response_model_exclude={'group'}
)
def get_machines(db: Session = Depends(get_db)):
return db.query(machine_models.Machines).all()
I would like to be able to set the response_model_exclude field value in the decorator directly from the API call. In practice I would like to have a query parameter on the function that allows me allows me to get the foreign key information or not.
To avoid having this situation:
#app.get(
"/machines",
response_model=List[machine_schemas.Machines],
response_model_exclude={'group'}
)
def get_machines(db: Session = Depends(get_db)):
return db.query(machine_models.Machines).all()
#app.get(
"/machines/all",
response_model=List[machine_schemas.Machines],
)
def get_machines_all(db: Session = Depends(get_db)):
return db.query(machine_models.Machines).all()
Is it possible to achieve this?
Thank you.
I solved the problem by creating a custom JSONResponse and using the jsonable_encoder function to exclude the foreign key fields.
#app.get(
"/machines/all",
response_model=List[machine_schemas.Machines],
)
def get_machines_all(get_foreign_keys: bool = True, db: Session = Depends(get_db)):
machines = db.query(machine_models.Machines).all()
if get_foreign_keys:
return machines
else:
return JSONResponse(jsonable_encoder(machines, exclude={'group'}))
Related
I have a many-to-many relationship in my DB design and I am having trouble with the POST aspect. I currently have a table called Loads, Containers, and Container_Loads (this is the intermediary table).
My question is this:
I want to be able to send a POST request into the ContainerLoad intermediary table and just update that table with the values it requires which are: Load ID (PK of the Load table), Container ID (PK of the Container Table) and # of pallets (unique field to the intermediary table). I am able to GET/retrieve the records just fine, but when I try to send a POST request with a payload such as
{
"id":3,
"pallets":"4",
"containerNumberId":5,
"loadNumberId":53
}
(where containerNumberID and loadNumberID are the existing keys in their respective tables), it seems that my code wants to create a whole new Load entry as well (as it asks me for the remaining fields of the Load model), where as I just want to create an entry in the intermediary table without creating a new entry in the Load table.
So for the purpose of my project, a load can be on many containers [imagine that it's split because all of it couldn't fit on one] and a container can belong to many loads.
My models.py looks like this:
class ContainerLoad(models.Model):
id = models.AutoField(primary_key=True)
load_number = models.ForeignKey(Load,on_delete=models.CASCADE)
container_number = models.ForeignKey(Container,on_delete=models.CASCADE)
pallets = models.CharField(blank=True,null=True,default=0,max_length=20)
class Meta:
db_table = 'ContainerLoad'
#load model shortened for brevity
class Load(models.Model):
id = models.AutoField(primary_key=True)
bnsf_container_number = models.ManyToManyField(Container, through='ContainerLoad',through_fields=('load_number','container_number'))
class Meta:
db_table = "Load"
class Container(models.Model):
id = models.AutoField(primary_key=True)
container_number = models.CharField(max_length=15)
in_use = models.BooleanField()
class Meta:
db_table = "Container"
my serializers.py currently looks like this, the commented out section is from me attempting to get the POST to work)
class ContainerLoadSerializer(WritableNestedModelSerializer):
# load_number_id = LoadSerializer(read_only=False)
# container_number_id = ContainerSerializer(read_only=False)
class Meta:
model = ContainerLoad
fields = "__all__"
depth = 2
class LoadSerializer(WritableNestedModelSerializer):
primary_driver = DriverSerializer(read_only=False)
second_driver = DriverSerializer(allow_null=True,read_only=False)
third_driver = DriverSerializer(allow_null=True,read_only=False)
bnsf_container_number = ContainerSerializer(read_only=False)
pickup_location = LocationSerializer(read_only=False)
delivery_location = LocationSerializer(read_only=False)
broker = BrokerSerializer(read_only=False)
booked_by = EmployeeSerializer(read_only=False)
class Meta:
model = Load
fields = '__all__'
depth = 1
class ContainerSerializer(serializers.ModelSerializer):
container_number = serializers.CharField()
in_use = serializers.BooleanField()
class Meta:
model = Container
fields = '__all__'
depth = 1
And finally the views.py
class ContainerLoadViews(APIView):
def get(self, request, id=None):
if id:
container = ContainerLoad.objects.get(id=id)
serializer = ContainerLoadSerializer(container)
return Response({"status": "success", "data": serializer.data}, status=status.HTTP_200_OK)
else:
containers = ContainerLoad.objects.all()
serializer = ContainerLoadSerializer(containers, many=True)
return Response({"status": "success", "data": serializer.data}, status=status.HTTP_200_OK)
def post(self, request):
serializer = ContainerLoadSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response({"status": "success", "data": serializer.data}, status=status.HTTP_200_OK)
else:
return Response({"status": "Error", "data": serializer.errors}, status=status.HTTP_400_BAD_REQUEST)
If you look at the code of the save method of the BaseSerializer class you will see this:
if self.instance is not None:
self.instance = self.update(self.instance, validated_data)
assert self.instance is not None, (
'`update()` did not return an object instance.'
)
else:
self.instance = self.create(validated_data)
assert self.instance is not None, (
'`create()` did not return an object instance.'
)
How you are not passing the instance in the post function:
serializer = ContainerLoadSerializer(data=request.data)
The save is always calling to create. You should do something like.
try:
instance = ContainerLoad.object.get(id=request.data['id'])
except:
instance = None
serializer = ContainerLoadSerializer(instance=instance, data = request.data)
You have depth is set to 2 in your ContainerLoadSerializer Meta class, which is telling the serializer to generate a nested representation of your models.
https://www.django-rest-framework.org/api-guide/serializers/#specifying-nested-serialization
The default ModelSerializer uses primary keys for relationships, but you can also easily generate nested representations using the depth option:
The depth option should be set to an integer value that indicates the depth of relationships that should be traversed before reverting to a flat representation.
If you remove the depth attribute, the serializer should default back to expecting a primary key value, which is your desired behaviour.
Your serializer should look something like this:
class ContainerLoadSerializer(serializers.ModelSerializer):
class Meta:
model = ContainerLoad
fields = "__all__"
The solution to this was that I needed a nested response when reading the data from the ContainerLoad table but a simple write (not nested) function when POSTing the data.
The solution was to use the to_representation and to_internal_value methods (https://www.django-rest-framework.org/api-guide/serializers/#overriding-serialization-and-deserialization-behavior) available within DRF to override the behavior of the serializers. Here is the code that now works for both GET and POST requests and it is no longer asking me for fields related to the Load or Container models when inserting data.
class ContainerSerializer(serializers.ModelSerializer):
container_number = serializers.CharField()
in_use = serializers.BooleanField()
class ContainerFieldSerializer(serializers.Field):
def to_internal_value(self,value):
return Container.objects.get(id=value)
def to_representation(self,instance):
return ContainerSerializer(instance=instance).data
class Meta:
model = Container
fields = '__all__'
depth = 1
I did the same for the Load Serializer.
and then for my ContainerLoad Serializer I just assign the FK fields to the new classes I created:
class ContainerLoadSerializer(serializers.ModelSerializer):
cl_container = ContainerSerializer.ContainerFieldSerializer()
cl_load = LoadSerializer.LoadFieldSerializer()
class Meta:
model = ContainerLoad
fields = "__all__"
depth = 2
I made a function to check if a specific user has permissions to make some action on a resource.
If the user has super permissions has_all_actions, has_all_resources it can make any action on any resource.
I don't know if i implemented the function well, but with some manual tests i made it's working.
def check_if_user_has_permissions(user= None, resource = None, action = None):
tmp = {}
tmp["user"] = user
tmp["resource"] = resource
tmp["action"] = action
data = []
has_permission = db.session.query(RoleMember.user_uid, Permission.resource, Permission.action). \
filter_by(user_uid=user). \
join(Permission, Permission.role == RoleMember.role).all()
if resource and action is not None:
if has_permission == "":
return False
else:
for info in has_permission:
for item in info:
data.append(item)
has_all_actions = "*"
has_all_resources = "machines/*"
if tmp["user"] in data and data[1] == has_all_resources and data[2] == has_all_actions:
return True
else:
if tmp["user"] in data and tmp["resource"] in data and tmp["action"] in data:
return True
else:
return False
else:
return False
This functions returns true of false depending if the user has permissions or not and returns always true if the user has super permissions: has_all_actions, has_all_resources.
My database is SqlAlchemy but at the moment I'am using this SQLALCHEMY_DATABASE_URI: "sqlite://" so i don't have real data stored in the db.
Now i want to test this function with #pytest.mark.parametrize, so I pass different parameters to the actual function and assert that is false or true depending on the data that is on the database.
So i think i also need to mock the database and add some data there so i can do this test? or is there a way without mocking the db?
Any examples i can follow? I'am lost in this.
Forgot to show my models:
class Role(db.Model):
__tablename__ = "roles"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(250), unique=True)
description = db.Column(db.String(1000))
permissions = db.relationship("Permission", lazy=True)
members = db.relationship("RoleMember", lazy=True)
class Permission(db.Model):
__tablename__ = "permissions"
id = db.Column(db.Integer, primary_key=True)
resource = db.Column(db.String(250))
action = db.Column(db.String(250))
role = db.Column(db.Integer, db.ForeignKey("roles.id"))
class RoleMember(db.Model):
__tablename__ = "role_members"
id = db.Column(db.Integer, primary_key=True)
user_uid = db.Column(db.String(250))
role = db.Column(db.Integer, db.ForeignKey("roles.id"))
So i found the answer, the problem was that my has_permission was returning a type [(" ")] and i was making the mock returning a type [" "].
So the solution was:
#patch("dev_maintenance.roles.db")
def test_check_if_user_has_permissions(db):
tmp = {}
tmp["user"] = "bender"
tmp["resource"] = "machine/1"
tmp["action"] = "delete"
##The mock
db.session.query.return_value.filter_by.return_value.join.return_value.all.return_value = [(tmp["user"], tmp["resource"], tmp["action"])]
## check if the function is returning True
assert check_if_user_has_permissions("bender","machine/1", "delete") == True
I have this models
class Post(Base):
__tablename__ = 'posts'
id = Column(Integer, primary_key=True)
content = Column(Text)
author = Column(Integer, ForeignKey('users.id'))
to_topic = Column(Integer, ForeignKey('topics.id'))
def __init__(self, content: str, author: int, to_topic: int) -> None:
self.content = content
self.author = author
self.to_topic = to_topic
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
username = Column(String(30))
email = Column(String(40))
password = Column(String(255))
registred_at = Column(DateTime, default=datetime.datetime.now)
last_logged_in = Column(DateTime)
last_login_attempt = Column(DateTime)
avatar = Column(Integer, ForeignKey('files.id'))
role = Column(String(20))
email_confirm = Column(Boolean, default=False)
children_forum = relationship('Forum')
children_topic = relationship('Topic')
children_post = relationship('Post')
And I am trying to get query that will contain Post.content, Post.author.username but how do i do this in sessions?
I tried
posts = db_session.query(Post, User).filter(
Post.to_topic == topic_id).with_entities(Post.content, Post.author.username)
but that doesnt work because author is just integer (id) so I expect that I need somehow get the author object and post object in one query, but I dont know how. In sql that would be easy just 2 queries but here I dont know how it´s done.
query_results = db_session.query(Post, User).\
join(User, Post.author == User.id).\
filter(
Post.to_topic == topic_id
).all()
I've never used with_entities, but I know this would give you a list of tuples where query_results[0] would be your Post instance, and query_results[1] would be your User instance.
EDIT: I believe you don't have to include the Post.author == User.id bit, but it's more legible if you're explicit with your joins.
I'm currently working on a big code base and i need to send emails from any potential module, that conduct in Circular Dependencies issues in python
so i tried to use apps.get_model() from django.apps but when serializers are declared the models are not ready.
So i'm trying to create a factory function who build the class at runtime instead of launch time
from rest_framework.serializers import ModelSerializer
def make_serializer(model: str, fields: tuple, options = None, **nested_fields) -> ModelSerializer:
"""Generate a new serializer "On the fly", so the model does not have to be imported at launch time.
"""
model_object = apps.get_model(model)
input_fields = fields
if options is None:
options = {}
class Serializer(ModelSerializer):
class Meta:
model = model_object
fields = input_fields
def create(self, validated_data):
# we won't permit to create data from thoses serializers.
raise NotImplementedError
# configure nested serializers.
for nested_field in nested_fields.values():
for key, nested_serializer_class in nested_field.items():
serializer_instance = nested_serializer_class(**options.get(key, {}))
print(model, key, serializer_instance)
setattr(Serializer, key, serializer_instance)
return Serializer
my tests models looks like
class Band(Model):
name = Charfield(max_length=255)
class Influencer(Model):
entity = Charfield(max_length=255)
class Submission(Model):
influencer = ForeignKey(Influencer, ...)
class Campaign(Model):
band = ForeignKey('band.Band', ...)
submissions = ManyToMany(Submission)
and my testing function is:
def test():
serializer = make_serializer(
model='submission.Campaign',
fields=['pk', 'submissions', 'band'],
options={'submissions': {'many': True}},
nested_fields={
'submissions': make_serializer(
model='submission.Submission',
fields=('influencer',),
nested_fields={
'influencer': make_serializer('influencer.Influencer', ('entity',))
},
),
'band': make_serializer('band.Band', ('name',))
}
)
return serializer
instead of having my fields correly with test()(Campaign.objects.last()).data i only got "pks" and my serialiser looks like:
Serializer():
pk = IntegerField(label='ID', read_only=True)
submissions = PrimaryKeyRelatedField(many=True, queryset=Submission.objects.all())
band = PrimaryKeyRelatedField(allow_null=True, queryset=Band.objects.all(), required=False)
i except and output like:
{
"pk": 1,
"band": {
"name": "BobMarley",
},
"submissions": [
{
"influencer": {"entity": "The influencer's name"}
}
]
}
but i got a ReturnDict containing:
{
"pk": 1,
"band": 523,
"submissions": [6, 7, 8]
}
thanks for your time
well after many headcaches i've found out that i CAN'T setattr on a class after it's declaration, so i use a trick based on a dict
def make_serializer(model: str, fields: tuple, options = None, **nested_fields) -> ModelSerializer:
"""Generate a new serializer "On the fly", so the model does not have to be imported at launch time.
"""
name = f'Serializer_{model}'
model_object = apps.get_model(model)
input_fields = fields
if options is None:
options = {}
def create(self, validated_data):
# we won't permit to create data from thoses serializers.
raise NotImplementedError
class Meta:
model = model_object
fields = input_fields
attrs = {"Meta": Meta}
# configure nested serializers.
for key, nested_serializer_class in nested_fields.items():
attrs[key] = nested_serializer_class(**options.get(key, {}))
attrs['create'] = create
return type(ModelDictSerializer)(name, (ModelDictSerializer,), attrs)
the syntax is something like:
campaign_serializer = make_serializer(
model='submission.Campaign',
fields=['pk', 'submissions', 'band'],
options={'submissions': {'many': True}},
submissions=make_serializer(
model='submission.Submission',
fields=('influencer',),
influencer=make_serializer('influencer.Influencer', ('entity',))
),
band=make_serializer('band.Band', ('name',))
)
and it's working like a charm:
Serializer_submission.Campaign(<Campaign: Campaign object (9665)>):
pk = IntegerField(label='ID', read_only=True)
submissions = Serializer_submission.Submission(many=True):
influencer = Serializer_influencer.Influencer():
entity = CharField(allow_blank=True, max_length=255, required=False)
band = Serializer_band.Band():
name = CharField(max_length=255)
i hope this will help someone else
so I was trying to implement CRUD functions on my code so i'm trying to create a session first but it keeps giving me an error "engine is not defined" so I tried to define it but I keep getting the same error. the code is creating a database 3 tables (user, account, place)using sqlalchemy ORM. what am I doing wrong?
from sqlalchemy import create_engine, Column, Integer, Boolean, String, ForeignKey, DateTime
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm.query.Query
Session = sessionmaker(bind=engine)
Base = declarative_base()
class User(Base):
__tablename__ = "user"
id = Column('id',Integer,primary_key=True , autoincrement=True)
username = Column('username',String , unique=True)
admin = Column('admin',Boolean )
password = Column('password' , String)
email = Column('email' , String , unique=True)
p_id = Column(Integer , ForeignKey('place.id'))
googleassist = Column('googleassist' , String)
account = relationship("Account", ## Cascade to delete associated account
back_populates='user', ## if that user was deleted.
cascade='all, delete-orphan')
class Account(Base):
__tablename__ = "account"
id = Column('id', Integer,primary_key=True,autoincrement=True)
user_id = Column(Integer , ForeignKey('user.id'))
class Place(Base):
__tablename__ = "place"
id = Column('id', Integer,primary_key=True,autoincrement=True)
p_location = Column('p_location' , String)
h_id = Column('h_id',Integer)
Session.configure(bind=engine)
engine = create_engine('sqlite:///withServer.db')
Base.metadata.create_all(engine)
session = Session(engine)
#creating objects
u1=User( id = '1' ,
username = 'ali',
admin = '1',
password = 'pass',
email = 'haifa#gmail.com'
)
session.add(u1)
session.commit()
#update
i = session.query(User)
i.password = 'passtoo'
session.add(i)
session.commit()
#delete
i = session.query(Account).filter(Account.id == '1').one()
session.delete(i)
session.commit()
indeed you just use engine without defining it first:
Session = sessionmaker(bind=engine)
you need to have the definition of your engine before that line. move
engine = create_engine('sqlite:///withServer.db')
before the first usage of engine.