Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/fernaper/kascade-orm

Python ORM for SQL Databases based on Pydantic
https://github.com/fernaper/kascade-orm

Last synced: 1 day ago
JSON representation

Python ORM for SQL Databases based on Pydantic

Awesome Lists containing this project

README

        



Kascade ORM



Python ORM for SQL Databases based on Pydantic





## What is Kascade ORM?

Kascade ORM is the next-geneneration ORM built on top of Pydantic in order to have the best integration with frameworks like FastAPI.

---

## Roadmap

Our plan is to give the users maximum access to SQL Databases inside Python code without making them hard to understand.
To do so, we plan to structure everything in `Objects`.

### Notes about Callable:

This util callables could be created directly in SQL or via Python.
If is it possible it is always created on SQL.

- cuid: Depends
- uuid: Depends
- random: Depends
- autoincrement: SQL Always
- utcnow: SQL Always
- now: SQL Always
- custom: Python Always

### Things to store per column:

- Type: `Type Hint`
- Name: `str`
- Unique: `bool`
- Optional: `bool`
- IsId: `bool`
- Default: `Any` or `Callable`
- OnUpdate: `None` or `Callable`

### Things to store per Relation:
- Table1: `Table`
- Table2: `Table`
- Table1Columns: `List[Column]`
- Table2Columns: `List[Column]`

### Things to store per Table:

- Columns: `List[Column]`
- CompoundUniques: `List[List[Column]]`
- Indexes: `List[Column]`
- Relations: `List[Relation]`

---

## Extra features planned to be added

1. If we have two tables we plan to substract them in order to detect differences between them. This whay we can easily manage `apply`s to update the tables.
2. Users should be capable to create fast Type Hints from their tables in order to allow returning for example an `User` without the `password` in FastAPI without needing to create a custom Schema that is just a duplication of the `User` schema without this field.
3. Important: Allow to generate the Python file with the current Database schema.

## This is an example of how we plan to create tables

```python
from pydantic import EmailStr
from kascade import ForeignKey, Table, Column, column_defaults

class Item(Table):
# Note that if it is called ID and is an int,
# this configuration is equivalente to the
# `User` table configuration
id: int
name: str
user_id: int

class User(Table):
name: str
email: EmailStr
password: str
id: Column = Column(
type=int,
unique=True,
default=column_defaults.autoincrement,
)
avatar: Optional[bytes] = None
items: ForeignKey = ForeignKey(
table=Item,
column='user_id',
)
```

This is just an idea, we could change it. Also, we are still thinking on the best way to manage relationships.

On this example the generated Python code (internally) will look similar to:

```python

from pydantic import EmailStr
from kascade import ForeignKey, Table, Column, column_defaults

class Item(Table):
id: int
name: str
user_id: int

@property
def user(self):
# Code to get user dynamically (or explicitly)
pass

class User(Table):
name: str
email: EmailStr
password: str
id: int
avatar: Optional[bytes] = None

@property
def items(self):
# Code to get items dynamically (or explicitly)
pass

```

This class will have also other methods based on Table ones.

---

Also, this is an example on how an end user will use this tables:

```python
import asyncio

from kascade import Kascade

async def main():
async with Kascade() as db:
user = await db.User.create({
name='Fernando Pérez',
email='[email protected]',
password='super-secure-kascade-password',
})

item = await db.Item.create({
name='Laptop',
# Only one of the following is needed
user_id=user.id,
user=user,
})

all_users_with_avatar = await db.User.find_many({
'where': {
'avatar': {
'not': None,
}
}
})

# In this case we know that for some reasson we need
# to query all items for each user,
# Therefore, in order to improve performance and
# avoid unexpected loads when querying items from
# each user with the code: all_kascade_emails[0].items
all_kascade_emails = await db.User.find_many({
'where': {
'email': {
'ends_with': '@kascade.com',
}
}
'include': {
'items': True,
}
})

# Example on how to define Type Hints
def item_example() -> Kascade.Item:
pass

# Example on how to skip some parameters
def user_example() -> kascade.User.exclude('password'):
pass

if __name__ == '__main__':
asyncio.run(main())

```

In order to suppor include or exclude we are going to solve it in a way similar to:

```python
from typing import get_type_hints
from pydantic import BaseModel, create_model

# `create_model` <--- this is the solution
# https://chat.openai.com/share/cfa989a8-7ac4-4508-abd8-f39ca5a17602

class Table(BaseModel):
a: int = 1
b: int = 2

@classmethod
def exclude(cls, *field_names):
annotations = get_type_hints(cls)
field_names = set(field_names)
for field_name in field_names:
annotations.pop(field_name, None)
fields_dict = cls.model_fields
new_fields = {
name: field for name, field in fields_dict.items() if name not in field_names
}
namespace = {'__annotations__': annotations}
new_class = type(cls.__name__, (BaseModel,), namespace)
new_class.__fields__ = new_fields
return new_class
```