Compare commits
No commits in common. "f1edcb8e5455ab67b50281316cd008c638ccebd3" and "de898dec6a4a808c82230304819f9cdcf2fbaee0" have entirely different histories.
f1edcb8e54
...
de898dec6a
@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "First blog post"
|
title: "First blog post"
|
||||||
date: 2021-12-25 02:54:08 +0300
|
date: 2021-12-25 02:54:08 +0300
|
||||||
classes: wide
|
|
||||||
excerpt: Hello, World!* So here I am and welcome to my first blog. Having a personal space on the Internet has been a dream for me for years and I am happy that it fi...
|
excerpt: Hello, World!* So here I am and welcome to my first blog. Having a personal space on the Internet has been a dream for me for years and I am happy that it fi...
|
||||||
---
|
---
|
||||||
<style>
|
<style>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "Stop cat-pipe'ing, You Are Doing It Wrong!"
|
title: "Stop cat-pipe'ing, You Are Doing It Wrong!"
|
||||||
date: 2022-01-01 18:00:00 +0300
|
date: 2022-01-01 18:00:00 +0300
|
||||||
classes: wide
|
|
||||||
tags: cat grep linux command-line
|
tags: cat grep linux command-line
|
||||||
---
|
---
|
||||||
```bash
|
```bash
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "Automatically Build and Deploy Your Site using GitHub Actions and Webhooks"
|
title: "Automatically Build and Deploy Your Site using GitHub Actions and Webhooks"
|
||||||
date: 2022-01-04 20:40:00 +0300
|
date: 2022-01-04 20:40:00 +0300
|
||||||
classes: wide
|
|
||||||
tags: github-actions github-webhooks ci-cd
|
tags: github-actions github-webhooks ci-cd
|
||||||
---
|
---
|
||||||
In this post I will explain how you can use GitHub to automate the build and deployment processes that you have. I am going to automate the deployment of this site but you can do whatever you want. Just understanding the basics will be enough.
|
In this post I will explain how you can use GitHub to automate the build and deployment processes that you have. I am going to automate the deployment of this site but you can do whatever you want. Just understanding the basics will be enough.
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
title: "Using ffmpeg for Simple Video Editing"
|
title: "Using ffmpeg for Simple Video Editing"
|
||||||
date: 2022-01-21 23:40:00 +0300
|
date: 2022-01-21 23:40:00 +0300
|
||||||
tags: cli ffmpeg
|
tags: cli ffmpeg
|
||||||
classes: wide
|
|
||||||
excerpt:
|
excerpt:
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "Creating a *Useless* User"
|
title: "Creating a *Useless* User"
|
||||||
date: 2022-02-27 16:40:00 +0300
|
date: 2022-02-27 16:40:00 +0300
|
||||||
classes: wide
|
|
||||||
tags: linux permissions privileges
|
tags: linux permissions privileges
|
||||||
---
|
---
|
||||||
## Story
|
## Story
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "SSH into Machine That Is Behind a Private Network"
|
title: "SSH into Machine That Is Behind a Private Network"
|
||||||
date: 2022-02-27 00:40:00 +0300
|
date: 2022-02-27 00:40:00 +0300
|
||||||
classes: wide
|
|
||||||
tags: ssh private-network remote-port-forwarding
|
tags: ssh private-network remote-port-forwarding
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "Never Get Trapped in Grub Rescue Again!"
|
title: "Never Get Trapped in Grub Rescue Again!"
|
||||||
date: 2022-03-03 03:46:00 +0300
|
date: 2022-03-03 03:46:00 +0300
|
||||||
classes: wide
|
|
||||||
tags: linux grub partition uefi
|
tags: linux grub partition uefi
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
title: "Confession Time"
|
title: "Confession Time"
|
||||||
date: 2022-04-08 18:46:00 +0300
|
date: 2022-04-08 18:46:00 +0300
|
||||||
last_modified_at: 2022-04-13
|
last_modified_at: 2022-04-13
|
||||||
classes: wide
|
|
||||||
tags: ssl
|
tags: ssl
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "Rant: Stop whatever you are doing and learn how licenses work"
|
title: "Rant: Stop whatever you are doing and learn how licenses work"
|
||||||
date: 2022-06-22 10:46:00 +0300
|
date: 2022-06-22 10:46:00 +0300
|
||||||
classes: wide
|
|
||||||
tags: copilot license github
|
tags: copilot license github
|
||||||
---
|
---
|
||||||
Recently, Github [announced](https://github.blog/changelog/2022-06-21-github-copilot-is-now-available-to-individual-developers/)
|
Recently, Github [announced](https://github.blog/changelog/2022-06-21-github-copilot-is-now-available-to-individual-developers/)
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
---
|
---
|
||||||
title: "Recap of 2022"
|
title: "Recap of 2022"
|
||||||
classes: wide
|
|
||||||
date: 2022-12-29 23:22:08 +0300
|
date: 2022-12-29 23:22:08 +0300
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "Hot-Reload Long Running Shell Scripts (feat. trap / kill)"
|
title: "Hot-Reload Long Running Shell Scripts (feat. trap / kill)"
|
||||||
date: 2023-01-16 00:48:08 +0300
|
date: 2023-01-16 00:48:08 +0300
|
||||||
classes: wide
|
|
||||||
tags: trap kill linux
|
tags: trap kill linux
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
---
|
---
|
||||||
title: "Conditionally load SQLAlchemy model relationships in FastAPI"
|
title: "Conditionally load SQLAlchemy model relationships in FastAPI"
|
||||||
date: 2024-08-04 12:55:01 +0300
|
date: 2024-08-04 12:55:00 +0300
|
||||||
classes: wide
|
|
||||||
tags: fastapi sqlalchemy pydantic
|
tags: fastapi sqlalchemy pydantic
|
||||||
---
|
---
|
||||||
|
|
||||||
Suppose you have a FastAPI app responsible for talking with database via sqlalchemy and retrieving data. The sqlalchemy models have some `relationship`s and the job of your app is exposing this database models with or without relationships based on the operation. You can't make use of relationship loading strategies (`'selectin'`, `'joined'` etc.) because FastAPI tries to convert the result to pydantic schemas and pydantic tries to load the relationship even though you don't need it to load for that specific operation. There are three things you can do:
|
Suppose you have a FastAPI app responsible for talking with database via sqlalchemy and retrieving data. The sqlalchemy models have some `relationship`s and the job of your app is exposing this database models with or without relationships based on the operation. You can't make use of relationship loading strategies (`'selectin'`, `'joined'` etc.) because FastAPI tries to convert the result to pydantic schemas and pydantic tries to load the relationship even though you don't need it to load for that specific operation. There are three things you can do:
|
||||||
|
|
||||||
1. *Define the loading strategy in your sqlalchemy models and create different pydantic schemas for the same entity including/excluding different fields. (i.e. if you don't need to load `Book.reviews` for specific endpoint, create a new pydantic schema for books without including `reviews`). After that, create different routes for returning correct schema. (`get_books_with_reviews`, `get_books_without_reviews` etc.)* <br/>
|
1. Define the loading strategy in your sqlalchemy models and create different pydantic schemas for the same entity including/excluding different fields. (i.e. if you don't need to load `Book.reviews` for specific endpoint, create a new pydantic schema for books without including `reviews`). After that, create different routes for returning correct schema. (`get_books_with_reviews`, `get_books_without_reviews` etc.)
|
||||||
This is the easiest option. Once you decided how each field should be loaded, you only need to create different pydantic schema for loading different fields. The downside is that you will have a lot of schemas and maintaining them will be hard.
|
This is the easiest option. Once you decided how each field should be loaded, you only need to create different pydantic schema for loading different fields. The downside is that you will have a lot of schemas and maintaining them will be hard.
|
||||||
2. *Create a pydantic schema with all fields and declare some fields as nullable. Do not load anything by default in sqlalchemy models (`lazy='noload'`). Create different routes returning the same schema but inside the routes, manually edit the `query.options` to load different fields.* <br/>
|
2. Create a pydantic schema with all fields and declare some fields as nullable. Do not load anything by default in sqlalchemy models (`lazy='noload'`). Create different routes returning the same schema but inside the routes, manually edit the `query.options` to load different fields.
|
||||||
This is what we were doing in our project. Only implementing the routes that we need 90% of the time was working fine for us. If we need more data, we were doing a second request to our API and merging the results. This becomes tedious after some time which is why we decided to move away from it.
|
This is what we were doing in our project. Only implementing the routes that we need 90% of the time was working fine for us. If we need more data, we were doing a second request to our API and merging the results. This becomes tedious after some time which is why we decided to move away from it.
|
||||||
3. *Somehow get the loading options as input from users of your API and return what is requested.* <br/>
|
3. Somehow get the loading options as input from users of your API and return what is requested.
|
||||||
|
|
||||||
That's it! If such endpoint exists all our problems would be solved. In this post, we will implement that!
|
That's it! If such endpoint exists all our problems would be solved. In this post, we will implement that!
|
||||||
|
|
||||||
![One endpoint to rule them all](/assets/images/sqlalchemy/ring.jpg)
|
![One endpoint to rule them all](/assets/images/sqlalchemy/ring.jpg)
|
||||||
@ -178,37 +178,17 @@ async def get_authors():
|
|||||||
async def get_reviews():
|
async def get_reviews():
|
||||||
session = Session(engine)
|
session = Session(engine)
|
||||||
query = session.query(Review)
|
query = session.query(Review)
|
||||||
return query.all()
|
result = query.all()
|
||||||
|
return result
|
||||||
|
|
||||||
```
|
```
|
||||||
Let's test to see if we are at the same point. Run `uvicorn app:app --reload` then perform a GET request for `/authors` endpoint. You should see two authors returned:
|
Let's test to see if we are at the same point. Run `uvicorn app:app --reload` then go to `http://127.0.0.1:8000/docs`. If everything went correctly you should see two authors returned when you perform a GET request on `/authors` endpoint.
|
||||||
|
![Example result](/assets/images/sqlalchemy/res.png)
|
||||||
```sh
|
|
||||||
|
|
||||||
❯ curl -X 'GET' \
|
|
||||||
'http://127.0.0.1:8000/authors' \
|
|
||||||
-H 'accept: application/json' | jq
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"name": "Author 1",
|
|
||||||
"books": [],
|
|
||||||
"awards": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"name": "Author 2",
|
|
||||||
"books": [],
|
|
||||||
"awards": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
Now, let's start implementing what's required to load different fields based on user input. First of all, we need to have a way to determine the relationships of our database models. Then we will use these relationships to generate pydantic schemas which represents loading options. We will then use these schemas as input to our endpoint and inside our endpoint, we will load required fields.
|
Now, let's start implementing what's required to load different fields based on user input. First of all, we need to have a way to determine the relationships of our database models. Then we will use these relationships to generate pydantic schemas which represents loading options. We will then use these schemas as input to our endpoint and inside our endpoint, we will load required fields.
|
||||||
|
|
||||||
|
|
||||||
1. Define a `RelationshipLoader` class in `models.py`.
|
1. Define a `RelationshipLoader` class in `models.py`.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from sqlalchemy.inspection import inspect
|
from sqlalchemy.inspection import inspect
|
||||||
@ -243,7 +223,7 @@ class RelationshipLoader:
|
|||||||
load_strategy = ( # if it is a one to one or many to one relationship we are loading it with selectinload strategy, else joinedload
|
load_strategy = ( # if it is a one to one or many to one relationship we are loading it with selectinload strategy, else joinedload
|
||||||
{"selectinload": [getattr(cls, rel.key)]}
|
{"selectinload": [getattr(cls, rel.key)]}
|
||||||
if rel.direction.name in ["MANYTOONE", "ONETOMANY"]
|
if rel.direction.name in ["MANYTOONE", "ONETOMANY"]
|
||||||
else {"joinedload": [getattr(cls, rel.key)]}
|
else {"joinedload": getattr(cls, rel.key)}
|
||||||
)
|
)
|
||||||
relationships[rel.key] = load_strategy
|
relationships[rel.key] = load_strategy
|
||||||
|
|
||||||
@ -254,9 +234,12 @@ class RelationshipLoader:
|
|||||||
[*exclude_classes, cls] # excluding the current class because we don't want bidirectional relationships to create infinite loop
|
[*exclude_classes, cls] # excluding the current class because we don't want bidirectional relationships to create infinite loop
|
||||||
)
|
)
|
||||||
for nested_rel, nested_strategy in nested_relationships.items():
|
for nested_rel, nested_strategy in nested_relationships.items():
|
||||||
((strategy_name, attr),) = load_strategy.items()
|
# if it is a nested relationship, I am assuming it should be loaded with selectinload.
|
||||||
((_, nested_attr),) = nested_strategy.items()
|
# i don't know a better way to handle this but this is working fine for us.
|
||||||
relationships[f"{rel.key}.{nested_rel}"] = {strategy_name: [*attr, *nested_attr]}
|
combined = load_strategy["selectinload"].copy()
|
||||||
|
combined.extend(nested_strategy["selectinload"])
|
||||||
|
combined = {"selectinload": combined}
|
||||||
|
relationships[f"{rel.key}.{nested_rel}"] = combined
|
||||||
|
|
||||||
return relationships
|
return relationships
|
||||||
|
|
||||||
@ -277,14 +260,21 @@ This is all we need to do for our sqlalchemy models. Now, let's implement the fu
|
|||||||
|
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from models import RelationshipLoader
|
||||||
from pydantic import create_model
|
from pydantic import create_model
|
||||||
|
|
||||||
|
|
||||||
|
def generate_load_strategies(sqlalchemy_model: type[RelationshipLoader]) -> dict[str, Any]:
|
||||||
|
return sqlalchemy_model.get_relationships()
|
||||||
|
|
||||||
|
|
||||||
def generate_pydantic_model(model_name, strategies: dict[str, Any]) -> type[BaseModel]:
|
def generate_pydantic_model(model_name, strategies: dict[str, Any]) -> type[BaseModel]:
|
||||||
pydantic_fields = dict.fromkeys(strategies, (bool | None, None))
|
pydantic_fields = dict.fromkeys(strategies, (bool | None, None))
|
||||||
return create_model(model_name, **pydantic_fields)
|
return create_model(model_name, **pydantic_fields)
|
||||||
```
|
```
|
||||||
|
|
||||||
And lastly, let's use this function in `app.py` to generate pydantic schemas and use it in our router:
|
And lastly, let's use these functions in `app.py` to generate pydantic schemas and use it in our router:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# app.py
|
# app.py
|
||||||
@ -315,14 +305,15 @@ async def get_reviews(options: ReviewLoadOptions):
|
|||||||
for field, value in options:
|
for field, value in options:
|
||||||
if value:
|
if value:
|
||||||
query = query.options(ReviewRelationships[field])
|
query = query.options(ReviewRelationships[field])
|
||||||
return query.all()
|
result = query.all()
|
||||||
|
return result
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
And that's all! If everything went fine, you should be able to load your database models with whatever fields you like:
|
And that's all! If everything went fine, you should be able to load your database models with whatever fields you like:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
❯ curl -X 'POST' \
|
❯ curl -S -X 'POST' \
|
||||||
'http://127.0.0.1:8000/authors' \
|
'http://127.0.0.1:8000/authors' \
|
||||||
-H 'accept: application/json' \
|
-H 'accept: application/json' \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
---
|
---
|
||||||
layout: single
|
layout: single
|
||||||
title: About
|
title: About
|
||||||
classes: wide
|
|
||||||
permalink: /about/
|
permalink: /about/
|
||||||
---
|
---
|
||||||
|
|
||||||
Hi, my name is **Şahin Akkaya**. I am a Computer Engineer graduated
|
Hi, my name is **Şahin Akkaya**. I am a 4th year student studying
|
||||||
from [ITU][itu]. I am a Free Software enthusiast, Python lover and
|
Computer Engineering at [ITU][itu]. I am a Free Software enthusiast,
|
||||||
perfectionist. I like to tinker things until they are *just right*.
|
Python lover and perfectionist. I like to tinker things until they
|
||||||
I also believe there is no such thing as perfect so I never stop.
|
are *just right*. I also believe there is no such thing as perfect so
|
||||||
I will do my best to make things better and I love doing it so far.
|
I never stop. I will do my best to make things better and I love doing
|
||||||
|
it so far.
|
||||||
|
|
||||||
> Roses are red \\
|
> Roses are red \\
|
||||||
Violets are blue \\
|
Violets are blue \\
|
||||||
|
BIN
assets/images/sqlalchemy/res.png
Normal file
BIN
assets/images/sqlalchemy/res.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 84 KiB |
@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
layout: single
|
layout: single
|
||||||
title: Contact
|
title: Contact
|
||||||
classes: wide
|
|
||||||
permalink: /contact/
|
permalink: /contact/
|
||||||
---
|
---
|
||||||
You can contact me via:
|
You can contact me via:
|
||||||
|
@ -3,6 +3,5 @@
|
|||||||
# To modify the layout, see https://jekyllrb.com/docs/themes/#overriding-theme-defaults
|
# To modify the layout, see https://jekyllrb.com/docs/themes/#overriding-theme-defaults
|
||||||
|
|
||||||
layout: home
|
layout: home
|
||||||
classes: wide
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user