Giter Club home page Giter Club logo

prisma-client-py's Introduction


Prisma Client Python

Type-safe database access for Python


What is Prisma Client Python?

Prisma Client Python is a next-generation ORM built on top of Prisma that has been designed from the ground up for ease of use and correctness.

Prisma is a TypeScript ORM with zero-cost type safety for your database, although don't worry, Prisma Client Python interfaces with Prisma using Rust, you don't need Node or TypeScript.

Prisma Client Python can be used in any Python backend application. This can be a REST API, a GraphQL API or anything else that needs a database.

GIF showcasing Prisma Client Python usage

Note that the only language server that is known to support this form of autocompletion is Pylance / Pyright.

Why should you use Prisma Client Python?

Unlike other Python ORMs, Prisma Client Python is fully type safe and offers native support for usage with and without async. All you have to do is specify the type of client you would like to use for your project in the Prisma schema file.

However, the arguably best feature that Prisma Client Python provides is autocompletion support (see the GIF above). This makes writing database queries easier than ever!

Core features:

Supported database providers:

  • PostgreSQL
  • MySQL
  • SQLite
  • CockroachDB
  • MongoDB (experimental)
  • SQL Server (experimental)

Support

Have any questions or need help using Prisma? Join the community discord!

If you don't want to join the discord you can also:

How does Prisma work?

This section provides a high-level overview of how Prisma works and its most important technical components. For a more thorough introduction, visit the documentation.

The Prisma schema

Every project that uses a tool from the Prisma toolkit starts with a Prisma schema file. The Prisma schema allows developers to define their application models in an intuitive data modeling language. It also contains the connection to a database and defines a generator:

// database
datasource db {
  provider = "sqlite"
  url      = "file:database.db"
}

// generator
generator client {
  provider             = "prisma-client-py"
  recursive_type_depth = 5
}

// data models
model Post {
  id        Int     @id @default(autoincrement())
  title     String
  content   String?
  views     Int     @default(0)
  published Boolean @default(false)
  author    User?   @relation(fields: [author_id], references: [id])
  author_id Int?
}

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
  posts Post[]
}

In this schema, you configure three things:

  • Data source: Specifies your database connection. In this case we use a local SQLite database however you can also use an environment variable.
  • Generator: Indicates that you want to generate Prisma Client Python.
  • Data models: Defines your application models.

On this page, the focus is on the generator as this is the only part of the schema that is specific to Prisma Client Python. You can learn more about Data sources and Data models on their respective documentation pages.

Prisma generator

A prisma schema can define one or more generators, defined by the generator block.

A generator determines what assets are created when you run the prisma generate command. The provider value defines which Prisma Client will be created. In this case, as we want to generate Prisma Client Python, we use the prisma-client-py value.

You can also define where the client will be generated to with the output option. By default Prisma Client Python will be generated to the same location it was installed to, whether that's inside a virtual environment, the global python installation or anywhere else that python packages can be imported from.

For more options see configuring Prisma Client Python.


Accessing your database with Prisma Client Python

Just want to play around with Prisma Client Python and not worry about any setup? You can try it out online on gitpod.

Installing Prisma Client Python

The first step with any python project should be to setup a virtual environment to isolate installed packages from your other python projects, however that is out of the scope for this page.

In this example we'll use an asynchronous client, if you would like to use a synchronous client see setting up a synchronous client.

pip install -U prisma

Generating Prisma Client Python

Now that we have Prisma Client Python installed we need to actually generate the client to be able to access the database.

Copy the Prisma schema file shown above to a schema.prisma file in the root directory of your project and run:

prisma db push

This command will add the data models to your database and generate the client, you should see something like this:

Prisma schema loaded from schema.prisma
Datasource "db": SQLite database "database.db" at "file:database.db"

SQLite database database.db created at file:database.db


๐Ÿš€  Your database is now in sync with your schema. Done in 26ms

โœ” Generated Prisma Client Python to ./.venv/lib/python3.9/site-packages/prisma in 265ms

It should be noted that whenever you make changes to your schema.prisma file you will have to re-generate the client, you can do this automatically by running prisma generate --watch.

The simplest asynchronous Prisma Client Python application will either look something like this:

import asyncio
from prisma import Prisma

async def main() -> None:
    prisma = Prisma()
    await prisma.connect()

    # write your queries here
    user = await prisma.user.create(
        data={
            'name': 'Robert',
            'email': '[email protected]'
        },
    )

    await prisma.disconnect()

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

or like this:

import asyncio
from prisma import Prisma
from prisma.models import User

async def main() -> None:
    db = Prisma(auto_register=True)
    await db.connect()

    # write your queries here
    user = await User.prisma().create(
        data={
            'name': 'Robert',
            'email': '[email protected]'
        },
    )

    await db.disconnect()

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

Query examples

For a more complete list of queries you can perform with Prisma Client Python see the documentation.

All query methods return pydantic models.

Retrieve all User records from the database

users = await db.user.find_many()

Include the posts relation on each returned User object

users = await db.user.find_many(
    include={
        'posts': True,
    },
)

Retrieve all Post records that contain "prisma"

posts = await db.post.find_many(
    where={
        'OR': [
            {'title': {'contains': 'prisma'}},
            {'content': {'contains': 'prisma'}},
        ]
    }
)

Create a new User and a new Post record in the same query

user = await db.user.create(
    data={
        'name': 'Robert',
        'email': '[email protected]',
        'posts': {
            'create': {
                'title': 'My first post from Prisma!',
            },
        },
    },
)

Update an existing Post record

post = await db.post.update(
    where={
        'id': 42,
    },
    data={
        'views': {
            'increment': 1,
        },
    },
)

Usage with static type checkers

All Prisma Client Python methods are fully statically typed, this means you can easily catch bugs in your code without having to run it!

For more details see the documentation.

How does Prisma Client Python interface with Prisma?

Prisma Client Python connects to the database and executes queries using Prisma's rust-based Query Engine, of which the source code can be found here: https://github.com/prisma/prisma-engines.

Prisma Client Python exposes a CLI interface which wraps the Prisma CLI. This works by downloading a Node binary, if you don't already have Node installed on your machine, installing the CLI with npm and running the CLI using Node.

The CLI interface is the exact same as the standard Prisma CLI with some additional commands.

Affiliation

Prisma Client Python is not an official Prisma product although it is very generously sponsored by Prisma.

Room for improvement

Prisma Client Python is a fairly new project and as such there are some features that are missing or incomplete.

Auto completion for query arguments

Prisma Client Python query arguments make use of TypedDict types. Support for completion of these types within the Python ecosystem is now fairly widespread. This section is only here for documenting support.

Supported editors / extensions:

  • VSCode with pylance v2021.9.4 or higher
  • Sublime Text with LSP-Pyright v1.1.196 or higher
  • PyCharm 2022.1 EAP 3 added support for completing TypedDicts
    • This does not yet work for Prisma Client Python unfortunately, see this issue
  • Any editor that supports the Language Server Protocol and has an extension supporting Pyright v1.1.196 or higher
user = await db.user.find_first(
    where={
        '|'
    }
)

Given the cursor is where the | is, an IDE should suggest the following completions:

  • id
  • email
  • name
  • posts

Performance

While there has currently not been any work done on improving the performance of Prisma Client Python queries, they should be reasonably fast as the core query building and connection handling is performed by Prisma. Performance is something that will be worked on in the future and there is room for massive improvements.

Supported platforms

Windows, MacOS and Linux are all officially supported.

Version guarantees

Prisma Client Python is not stable.

Breaking changes will be documented and released under a new MINOR version following this format.

MAJOR.MINOR.PATCH

New releases are scheduled bi-weekly, however as this is a solo project, no guarantees are made that this schedule will be stuck to.

Contributing

We use conventional commits (also known as semantic commits) to ensure consistent and descriptive commit messages.

See the contributing documentation for more information.

Attributions

This project would not be possible without the work of the amazing folks over at prisma.

Massive h/t to @steebchen for his work on prisma-client-go which was incredibly helpful in the creation of this project.

This README is also heavily inspired by the README in the prisma/prisma repository.

prisma-client-py's People

Contributors

adriangb avatar anand2312 avatar bryancheny avatar dependabot[bot] avatar dv1x3r avatar ezorita avatar fisher60 avatar higherorderlogic avatar izeye avatar jacobdr avatar kafai-lam avatar kfields avatar kivo360 avatar leejayhsu avatar leon0824 avatar lewoudar avatar matyasrichter avatar nesb1 avatar oreoxmt avatar pre-commit-ci[bot] avatar q0w avatar renovate[bot] avatar rhoboro avatar robertcraigie avatar tooruu avatar tyteen4a03 avatar yezz123 avatar zspine avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

prisma-client-py's Issues

Add support for python3.10

Problem

The release candidate for python3.10 has been released, as such we should run tox under py310 as well.

Progress on this issue is being tracked under the wip/python3.10 branch

Additional context

I have already tried running tests with python3.10 and all but 3 passed straight off the bat, the only tests that didn't pass were due to internal changes that the tests relied on, not due to a bug in prisma.

However, the blocker for this is numerous amount of warnings that are generated and I'm not going to consider prisma as supporting python3.10 until all of these warnings have been resolved

../../../../../private/tmp/tox/prisma-client-py/py310/lib/python3.10/site-packages/prisma/generator/generator.py:6
  /private/tmp/tox/prisma-client-py/py310/lib/python3.10/site-packages/prisma/generator/generator.py:6: DeprecationWarning: The distutils package is deprecated and slated for removal in Python 3.12. Use setuptools or check PEP 632 for potential alternatives
    from distutils.dir_util import copy_tree

tests/test_batch.py::test_base_usage
  prisma-client-py/tests/conftest.py:39: DeprecationWarning: There is no current event loop
    return asyncio.get_event_loop()

tests/test_batch.py: 17 warnings
tests/test_client.py: 1 warning
tests/test_count.py: 2 warnings
tests/test_create.py: 4 warnings
tests/test_delete.py: 2 warnings
tests/test_delete_many.py: 1 warning
tests/test_find_first.py: 3 warnings
tests/test_find_many.py: 3 warnings
tests/test_find_unique.py: 3 warnings
tests/test_misc.py: 1 warning
tests/test_raw_queries.py: 9 warnings
tests/test_update_many.py: 1 warning
tests/test_upsert.py: 1 warning
tests/test_filters/test_datetime.py: 1 warning
  prisma-client-py/tests/utils.py:260: DeprecationWarning: There is no current event loop
    return asyncio.get_event_loop().run_until_complete(coro)

tests/test_batch.py::test_base_usage
tests/test_cli/test_dev.py::test_playground
  /private/tmp/tox/prisma-client-py/py310/lib/python3.10/site-packages/coverage/pytracer.py:194: DeprecationWarning: currentThread() is deprecated, use current_thread() instead
    self.thread = self.threading.currentThread()

tests/test_http.py::test_library_property
  /private/tmp/tox/prisma-client-py/py310/lib/python3.10/site-packages/prisma/_aiohttp_http.py:44: DeprecationWarning: There is no current event loop
    loop = asyncio.get_event_loop()

Add support for generating nested partial types

When working with relational fields it would be useful to be able to generate partial types for the relation, for example

model User {
  id      String   @default(cuid()) @id
  name    String
  email   String?
  profile Profile?
}

model Profile {
  id       String   @default(cuid()) @id
  bio      String
  views    Int
  user     User   @relation(fields:  [user_id], references: [id])
  user_id  String
}
Profile.create_partial('ProfileViews', include={'views'})
User.create_partial('UserWithViews', required={'profile'}, types={'profile': 'ProfileViews'})

Resulting in

class ProfileViews:
    views: int

class UserWithViews:
    ...
    profile: 'ProfileViews'

Remove transformation of field names

Automatically transforming field names introduces internal complexity that is not worth the value it provides to developers. This complexity leads to bugs such as #3, which are hard to fix and very confusing for developers to get.

Additionally having to transform aliases also decreases performance as we have to do at least two passes through all the data passed to be able to generate a query.
We still have to iterate through all fields in order to transform global aliases, e.g. startswith -> starts_with

Automatically transforming aliases can also lead to a confusing developer experience as field names in the schema don't match the field names in the client.

If a database field name is outside the developers control they can simply use prisma's @map function to map the field name to python case.

Public Release Epic

  • Loosen dependency version requirements
  • Use GitHub actions
  • Add warning if binaryTargets option is used
  • Add contributing docs (postgres uri required)
  • Add ARCHITECTURE.md (look at esbuild)
  • #36
  • type check tests with pyright
  • move package to src directory
  • add makefile
  • add support for filtering by relations
  • ensure all tests have docstrings (see blog post)
  • add option to enable logging queries
  • make pytest-pyright public
  • online runnable example, gitpod? (Also add to docs and readme)
  • create a template repository
  • #34
  • rename master to main
  • test typesafety on CI
  • update readme
  • add attributions

Docs

  • add badges to readme and docs
  • document action methods
  • document raw queries
  • switch quickstart to pyright
  • indent schemas to 2 spaces not 4
  • add filtering by types and relations to the common operations page
  • add features and roadmap to readme and docs
  • command line
  • version guarantees
  • windows is not supported
  • rename reference/common to operations or similar

Release order

  • Update all install messages to use PyPi
  • Public repo
  • add read the docs config
  • Publish to read the docs
  • Update references to docs website
  • Publish to PyPi
  • Create a GitHub release, go to https://github.com/RobertCraigie/prisma-client-py/releases
  • make templates and quickstart repository public
  • Update template and quickstart repository to link to docs website

Documentation references to update:

Add support for including count of relational fields

Problem

Prisma supports including the count of a relational field, so we should too.

const users = await prisma.user.findMany({
  include: {
    _count: {
      select: { posts: true },
    },
  },
})

which returns an object like this

{
  id: 1,
  email: '[email protected]',
  name: 'Alice',
  _count: { posts: 2 }
}

Prisma also have support for filtering by the count of the relation, see https://www.prisma.io/docs/concepts/components/prisma-client/aggregation-grouping-summarizing#filter-the-relation-count

Model aliases can clash

Problem

For example, a model defines two relational fields, the first named "categories" that references a "CustomCategories" model and the second named "posts" that references a "Post" model and in the "Post" model a relational field named "categories" is defined that references a "Categories" model. This setup will result in incorrect aliases being defined for one of the categories fields.

This issue was not fixed in the original design as it is a weird edge case that can be solved by the end user by mapping the troublesome fields.

Possible Solution

Each recursive model reference could be namespaced, for example, given the relation user -> posts -> categories aliases should be generated as such

{
  "": {},
  "posts": {
    "categories": {}
  }
}

instead of the current implementation

{
  "": {},
  "posts": {},
  "categories": {}
}

Add support for Json and filtering by Json

Problem

Since v2.23.0, Prisma has had experimental support for filtering by Json, we should officially support the Json type and filtering by it.

Suggested Solution

Due to limitations with our query builder we will not be able to serialize json values correctly as they are treated like any other field type. We could get it working by naively checking field types and adding special handling for Json fields however I don't like this.

Create a new python type Json that must be used to encapsulate any json data, e.g.

from prisma import Json

user = await client.user.create(
    data={
        'name': 'Robert',
        'meta': Json(country='Scotland')
    },
)
user = await client.user.create(
    data={
        'name': 'Robert',
        'meta': Json(['foo', 'wow'])
    },
)

Something like:

class Json:
    @overload
    def __init__(self, **kwargs: Serializable) -> None:
        ...
    
    @overload
    def __init__(self, data: Serializable) -> None:
        ...

There is another problem with this however, pydantic supports json types which would make working with the returned data much easier.

Load dotenv files before client connection

Problem

Prisma schema files can use the env("NAME") function to replace values with environmental variables, this is handled for us by prisma when generating the client or running migration commands but is not when running the query engine

Suggested solution

Use python-dotenv to load environmental variables, this should update os.environ as users may want to utilise their dotenv variables in their own code

Add full support for filtering by relations

Problem

Currently only support nested filtering (through the include argument), this means that even if nothing matches the nested filter the base filter still passes, which could be confusing.

Suggested solution

Add relational fields to WhereInput, e.g.

model Post {
  ...
  categories Category[] @relation(references: [id])
}

model Category {
  id    Int    @id @default(autoincrement())
  posts Post[] @relation(references: [id])
  name  String
}
class PostWhereInput(TypedDict, total=False):
	...
	categories: Optional['CategoryListRelationalFilter']

class CategoryListRelationalFilter(TypedDict, total=False):
	every: Optional['CategoryWhereInput']
	some: Optional['CategoryWhereInput']
	none: Optional['CategoryWhereInput']

Additional context

Playground schema screenshot

The take argument should affect the return type

For example, the following will not raise an index error in mypy

user = await client.user.find_unique(where={'id': 1}, include={'posts': {'take': 1}})
assert user is not None
post = user.posts[2]

One solution would be to modify the return type to a Tuple of the same length as the take argument, not sure how I feel about doing this as I do not want the runtime types to differ that much from the static types but I also don't want to have to convert all lists that we return to tuples at runtime either.

There might be some hacky way for overriding the getitem check mypy does.

Add support for selecting fields

Problem

A crucial part of modern and performant ORMs is the ability to choose what fields are returned, Prisma Client Python is currently missing this feature.

Mypy solution

As we have a mypy plugin we can dynamically modify types on the fly, this means we would be able to make use of a more ergonomic solution.

class Model(BaseModel):
  id: str
  name: str
  points: Optional[int]

class SelectedModel(BaseModel):
  id: Optional[str]
  name: Optional[str]
  points: Optional[int]

ModelSelect = Iterable[Literal['id', 'name', 'points']]

@overload
def action(
	...
) -> Model:
	...

@overload
def action(
	...
	select: ModelSelect
) -> SelectedModel:
	...

model = action(select={'id', 'name'})

The mypy plugin would then dynamically remove the Optional from the model for every field that is selected, we might also be able to remove the fields that aren't selected although I don't know if this is possible.

The downside to a solution like this is that unreachable code will not trigger an error when type checking with a type checker other than mypy, e.g.

user = await client.user.find_first(select={'name'})
if user.id is not None:
  print(user.id)

Will pass type checks although the if block will never be ran.

EDIT: A potential solution for the above would be to not use optional and instead use our own custom type, e.g. maybe something like PrismaMaybeUnset. This has its own downsides though.

EDIT: I also think we may also want to support setting a "default include" value so that relations will always be fetched unless explicitly given False. This will not change the generated types and they will still be Optional[T].

Type checker agnostic solution

After #59 is implemented the query builder should only select the fields that are present on the given BaseModel.

This would mean that users could generate partial types and then easily use them to select certain fields.

User.create_partial('UserOnlyName', include={'name'})
from prisma.partials import UserOnlyName

user = await UserOnlyName.prisma().find_unique(where={'id': 'abc'})

Or create models by themselves

class User(BaseUser):
  name: str

user = await User.prisma().find_unique(where={'id': 'abc'})

This will make typing generic functions to process models more difficult, for example, the following function would not accept custom models.:

def process_user(user: User) -> None:
  ...

It could however be modified to accept objects with the correct properties by using a Protocol.

class UserWithID(Protocol):
  id: str

def process_user(user: UserWithID):
  ...

Add support for batching queries

Problem

In some situations it is desirable that a write operation being committed is dependant on other write operations succeeding, for example, if you update 200 users within a transaction, each update must succeed - if not, all changes are rolled back and the transaction fails as a whole.

Suggested solution

We cannot support the same syntax that prisma does as we need to support a synchronous API as well

This is a good solution as it can easily be modified to support dependencies between write operations if it is added to the prisma query engine, #1844.

The only potential problem with this solution is that we are creating a new client reference which would potential be difficult for some users to implement but I cannot think of a way to do this while maintaining type safety.

async

async with client.batch_() as tx:
    tx.user.create({'name': 'Robert'})
    tx.user.create({'name': 'Bob'})

tx = client.batch_()
tx.user.create({'name': 'Robert'})
tx.user.create({'name': 'Bob'})
await tx.commit()

sync

with client.batch_() as tx:
    tx.user.create({'name': 'Robert'})
    tx.user.create({'name': 'Bob'})

tx = client.batch_()
tx.user.create({'name': 'Robert'})
tx.user.create({'name': 'Bob'})
tx.commit()

Additional context

Implementation notes

As we have to type every action to return None there will be some refactoring to support it, but it should be possible with generics and if not we can always duplicate the whole client/actions classes with Jinja

Relational field access without inclusion should error

Problem

The if branch block will never be ran, this should be caught by a type checker as it is a logical error.

As this is not possible to represent with standard python types this will need to be a supported type checker only feature.

user = await client.user.find_unique(where={'id': 'abc'})
if user.posts is not None:  # this should error when static type checking
  print(user.posts[0])

user = await client.user.find_unique(where={'id': 'abc'}, include={'posts': True})
if user.posts is not None:  # this is now valid
  print(user.posts[0])

However the following situation must still pass, this might make it very difficult (or impossible) to implement.

from prisma.models import User

def display_user_info(user: User) -> None:
  print(user.id)
  if user.posts is not None:
    print('posts: ' + ', '.join(post.id for post in user.posts))

user = await client.user.find_unique(where={'id': 'abc'})
display_user_info(user)

RFC: API Redesign

Currently the API is a near one to one mapping of the Prisma TypeScript API. This design was used so that the initial implementation would be easier and to ensure that advanced queries could be easily generated as I was not fully aware of the extent of the existing Prisma API before starting work on this project.

However, in my opinion using massive nested dictionaries as arguments is not very pythonic and has the added downside of no auto completion support which is one of the biggest appeals for using Prisma.

RFC

The questions posed by this RFC are:

  • Should the API be refactored?
  • If so, is the suggested implementation up to standard?

New API Proposal

Have a suggestion for any improvements?
Please add a comment below!

Notes:

  • I haven't implemented a POC for this API proposal, as such some operations might not be possible to type in python leading to changes from the proposed API.
# Synchronous clients must append `.run()` to execute queries, e.g.
blog = Blog(name='Beatles Blog')
blog.save().run()

# Asynchronous clients can optionally append `.run()` to execute queries, e.g.
await blog.save()
await blog.save().run()
# TODO:
- atomic updates

Reference

# base operations
from prisma.models import Blog

# create
blog = Blog(name='Beatles Blog', tagline='All the latest Beatles news.')
await blog.save().run()
await blog.save()

# update and upsert
blog.name = 'Queen Blog'
await blog.save().run()

# delete
await blog.delete()
await blog.delete().run()
# finding objects
from prisma.models import Blog
from prisma.querying import constraint

# find_unique
b = await Blog.objects.get(id=1).run()

# fetching relations
b = await Blog.objects.get(id=1).include(entries=True)
b = await Blog.objects.get(id=1).include().entries().where(foo=True)

# multiple relational fields
b = (
    await Blog.objects.get(id=1)
    .include()
        .entries()
            .where(foo=True)
            .parent()
        .other_relational_field()
)

# nested relational fields
b = (
    await Blog.objects.get(id=1)
    .include()
        .entries()
            .where(foo=True)
            .include(another_relational_field=True)
            .parent()
        .other_relational_field()
)

# find_many
blogs = await Blog.objects.all().run()
blogs = await Blog.objects.where(name='Beatles Blog').run()
blogs = await Blog.objects.where(name=constraint(contains='Blog')).run()
blogs = await Blog.objects.where(views=constraint(lt=10)).run()

# find_first
# same as find_many, replace .run() with .run(first=True)
blog = await Blog.objects.where(name='Beatles Blog').run(first=True)
# deleting objects
from prisma.models import Blog
from prisma.querying import constraint

# delete
blog = Blog.partial(id=1)
await blog.delete()

# delete_many
blogs = await Blog.objects.where(name='Beatles Blog').delete().run()

# TODO: more examples
# creating records
blog = Blog(name='My Cool Blog')
await blog.save()

blogs = Blog.objects.bulk_create(Blog(name='Another Blog'), Blog(name='Second Blog'))
# updating records
blog = Blog(id=1, name='My Cool Blog')
blog.name = 'My actual blog name'
await blog.save()

blogs = Blog.objects.bulk_update(Blog(id=2, name='First Blog'), Blog(id=3, name='Second Blog'))
# aggregation queries
total = await Blog.objects.count().where(views=constrained(gt=3))
total = await Blog.objects.count().cursor(id=3)
# batching queries
query1 = Blog.objects.get(id=1)
query2 = Blog.objects.where(views=constrained(lte=0))
await prisma.batch(query1, query2).run()
await prisma.batch(query1, query2)

# not implemented in the current API yet
async with prisma.transaction():
     blog = await Blog.objects.get(id=1)
     older = await Blog.objects.where(created_at=constrained(lt=blog.created_at))
# classmethods

class PartialBlog(BaseModel):
  id: int

class Blog(PartialBlog):
  @classmethod
  def partial(cls, *, id: int) -> 'PartialBlog':
    """Create a partial representation of this model with only the unique field present,
    useful for delete() queries.
    """
Expand to show implementation notes
from prisma.models import Blog

blog = Blog(name='My Blog')

# will create the record and modify the model in-place
await blog.save()
# will now call upsert as we have a primary key
await blog.save()

# .run() should only take a `first` argument for querying
# as otherwise it is ambiguous whether or not `first` being True
# would only modify the first record that matches or return the
# first record in the result
# e.g.
# valid
await Blog.objects.where(name='Beatles Blog').run(first=True)
# invalid
await Blog.objects.where(name='Beatles Blog).delete().run(first=True)

First class support for GraphQL frameworks

Problem

Prisma + GraphQL is a very good combination, we should at the very least provide examples for using Prisma Client Python with a GraphQL framework and at the most we should provide code generation support for basic CRUD queries.

List of potential GraphQL frameworks:

Suggested solution

  • Add examples for using GraphQL frameworks with prisma
  • Create extension modules for generating GraphQL framework schemas

The generated schemas and objects must be very easily extendable.

Add support for native query engine bindings

Problem

Prisma has experimental support for natively binding the query engine to the node client, this reduces the overhead between the client and the rust binary, improving performance.

We should look into whether or not this is feasible for us to do as well.

Suggested solution

Would probably make use of Py03 and Py03-asyncio I don't know how feasible this is yet but if this does end up shipping we would have to bundle the rust binary with the package either using wheels or a build extension as providing downloads for rust binaries is not something that would be feasible for me to provide.

Maybe related, should look into cibuildwheel for packaging, see uvloop for an example using GitHub actions

Status

Moved status to #165

Add a data validator

Problem

Transforming unknown/untrusted data into valid prisma input types can be a bit annoying, we should add a function that takes a type and data and validates the data against the type.

Suggested solution

def validate(type_: Type[T], data: Any) -> T:
    ...

We might be able to make use of pydantics dynamic create model feature and if not we can just roll our own validation.

def foo(data: Dict[str, Any]) -> User:
    create = prisma.validate(UserCreateInput, data)
    return client.user.create(data=create)

Improve editor experience

Problem

One of the biggest reasons for using an ORM is the autocomplete value that it provides, currently all of our query arguments will not be autocompleted, this is an issue as it greatly decreases user experience.

Users can only see what arguments are required when constructing the dictionary and will have to jump to the definition to get the full list of arguments.

Suggested solution

This is a tracking issue as the solution for this can only be implemented outside of this repository.

List of language servers / editor extensions that should support this:

  • pylance
  • jedi
  • Please comment if there are any other projects that should be added

Alternatives

I did consider refactoring the entire client API (#7) however I've decided against this as the current API is very unique to prisma and in my opinion makes complicated queries more readable than other ORMs such as SQLAlchemy.

Add support for setting a field to null

Problem

Optional database fields can be set to null, we currently don't support this as our query builder automatically removes None values and does not include them in the generated query.

Suggested solutions

There are currently three solutions being considered

Refactor the query builder to include type information

We will then be able to decide whether or not the field should be removed or not depending on the current context.

Refactor TypedDict optional fields

We currently mark a lot of fields as Optional when maybe they shouldn't be.

After this we can include all None values in generated queries as we can assume them to be valid.

Add a Null type

Whenever the query builder encounters this type it will include it in the generated query instead of discarding it.

A disadvantage of this approach is that it could be confusing for users as fields that they are explicitly setting to None would be discarded from the query.

from prisma.types import Null

await client.post.update(
    data={
        'nullable_field': Null(),
    },
    where={
        'id': 'abc',
    },
)

Add create_many action method

The createMany mutation was recently released behind a preview feature flag in v2.16.0

Should look something like

def create_many(
    self,
    data: List[{{ model.name }}CreateInput],
    skip_duplicates: bool
):
     ...

Optimise generated types

In order to circumvent mypy not supporting recursive types we duplicate every would-be recursive type. While this works it leads to a massive number of types being generated which slows down mypy considerably.

This appears to be unavoidable but we can improve performance by only generating types that will actually be used (we currently generate every possible relational type) and potentially re-using types.

We should also add a note to the docs somewhere that mypy performance can be improved by decreasing the depth of generated recursive types.

Add generator helpers

Problem

Creating custom Prisma generators in python requires a lot of knowledge on the internal workings of Prisma and a considerable amount of boilerplate, we should add helpers that will make custom python generators easy.

Suggested solution

from prisma.generator import BaseGenerator, Manifest, Data

class MyGenerator(BaseGenerator):
    def get_manifest(self) -> Manifest:
        ...

    def generate(self, data: Data) -> None:
        ...

MyGenerator.invoke()

Additional Context

We should also refactor the internal generator to use this helper as well so that custom generators can make use of our utilities such as rendering templates and generation safety.

Namespace client methods

Problem

Model names can clash with methods like connect(), query_first() etc

Suggested solution

We should namespace these methods behind an attribute like prisma and then validate that model names won't clash with that.

For example

await client.connect()

would turn into

await client.prisma.connect()

Installing from a local package results in the new package being polluted

Bug description

Installing from a local package that has been generated results in the new installation being polluted by the previous installation.

How to reproduce

This is most easily reproducible by mixing virtual environments and http dependencies.

git clone https://github.com/RobertCraigie/prisma-client-py prisma-client-py
cd prisma-client-py
python3 -m venv .venv && source .venv/bin/activate
pip install -U -e .[aiohttp]
prisma generate
deactivate
mkdir sync && cd sync
python3 -m venv .venv && source .venv/bin/activate
pip install -U ..[requests]
prisma --help ย # import error: aiohttp
datasource db {
  provider = "sqlite"
  url      = "file:dev.db"
}

generator db {
  provider = "prisma-client-py"
  http     = "aiohttp"
}

model User {
  id Int @id
}

WhereUnique should be marked with all keys being required

Bug description

Currently the following query, while obviously logically not sound, will pass type checks.

await client.post.find_unique(where={})

Expected behavior

The above query should fail when type checking.

How to fix

The TypedDict type should be marked with total=True, this was not the case initially due to unknown context, I do not know how prisma handles multiple fields being unique or a unique constraint spanning multiple fields

Add support for full text search for PostgreSQL

https://www.prisma.io/docs/concepts/components/prisma-client/full-text-search

Suggested solution

Something like the following, search should be added to StringFilter and must be an instance of String.

Should be noted that I'm not stuck on String being the name.

# NOTE: actual API is still TODO
from prisma.querying import String

await client.post.find_first(
    where={
        'content': {
            'search': String.contains('cat', 'dog'),
        },
    },
)
await client.post.find_first(
    where={
        'content': {
            # for anything we don't explicitly support
            'search': String.raw('fox \| cat'),
        },
    },
)

String filter case sensitivity

Problem

It would be useful to expose a method for filtering by either case sensitive or case insensitive.

Suggested solution

I do not know how this would be implemented as I don't know how it is currently implemented in prisma.

Add support for transactions

Prisma has preview support for interactive transactions.

The base work for this has already been done in the wip/transactions branch, some kinks need to be ironed out.

Status

  • Model-based access support
  • Protect against namespace collision

Model-based access

We could refactor the internal strategy to register and retrieve clients to use contextvars. However we should only use contextvars for transactions as otherwise it would break certain async use cases like the python -m asyncio REPL.

Add support for middleware

https://www.prisma.io/docs/concepts/components/prisma-client/middleware

client = Client()

async def logging_middleware(params: MiddlewareParams, next: NextMiddleware) -> MiddlewareResult:
    log.info('Running %s query on %s', params.action, params.model)
    yield await next(params)
    log.info('Successfully ran %s query on %s ', params.action, params.model)

async def foo_middleware(params: MiddlewareParams, next: NextMiddleware) -> MiddlewareResult:
    ...
    result = await next(params)
    ...
    return result

client.use(logging_middleware, foo_middleware)

Should Support

  • Yielding result
  • Returning result

Add support for scalar lists

Problem

PostgreSQL supports scalar array fields, e.g.

model User {
    ...
    emails String[]
}

Suggested solution

We should support updating and filtering scalar list fields

Additional context

WIP branch: wip/scalar-lists

Add client option to log SQL queries

Problem

Performance issues with ORMs can be hard to debug, we should add an option to log generated SQL queries.

Suggested solution

Don't know how this would be implemented as it seems like we'd have to spawn a new thread to capture and then filter the output from the query engine as the query engine logs contain a lot of noise that we don't want to send to users.

Add support for generic generation data

Problem

One area that this is needed for is for custom generators in order to be able to pass custom config options e.g.

generator db {
  provider = "my-prisma-generator"
  my_value = 1
}

Will raise an error as my_value is not a valid field in the Prisma Client Python config.

Another reason this is needed is to optionally bypass validation checks for python only features such as restricting certain field / model names.

Suggested solution

The Data model should be refactored to be a generic model instead of a normal pydantic model.

We would then need a method for exposing custom models to the generator, I do not know the best solution for this at the moment.

Should be noted that generic models are python3.7+ only.

Add a function for casting to magic types

Problem

Referencing magic types (types that are modified by a plugin) can be annoying as it is not possible to properly represent them.

We should add a function similar to typing.cast that type checker plugins will modify the return type like it is an action method.

def foo(user: User) -> None:
  print(user.posts[0])  # error: posts could be none

Suggested solution

def prisma_cast(model: BaseModelT, include: Dict[str, Any]) -> BaseModelT:
	return model
UserWithPosts = prisma_cast(User, include={'posts': True})

def foo(user: UserWithPosts) -> None:
  print(user.posts[0])  # valid

Improve type checking experience using a type checker without a plugin

Problem

For example, using pyright to type check would result in false positive errors that are annoying to fix when including 1-to-many relational fields.

user = await client.user.find_unique(where={'id': '1'}, include={'posts': True})
assert user is not None
for post in user.posts:
    ...
error: Object of type "None" cannot be used as iterable value (reportOptionalIterable)

This is a false positive as we are explicitly including posts they will never be None

NOTE: false positive due to our types, not a bug in pyright

Suggested solution

1-to-many relational fields should not be typed as optional, instead they should default to an empty list, however we should still error if the field is accessed without explicit inclusion for supported type checkers.

class User(BaseModel):
    ...
   posts: List[Post] = Field(default_factory=list)

Should be noted that we cannot do this for 1-to-1 relational fields.

Alternatives

The issue can be circumvented albeit using ugly / redundant methods

user = await client.user.find_unique(where={'id': '1'}, include={'posts': True})
assert user is not None
for post in cast(List[Post], user.posts):
    ...
user = await client.user.find_unique(where={'id': '1'}, include={'posts': True})
assert user is not None
assert user.posts is not None
for post in user.posts:
    ...

Enabling filtering by relational fields causes mypy SIGKILL

Bug description

Commit 46e74f0 causes mypy to be sent a SIGKILL.

How to reproduce

Running tox -e lint on the above commit will either cause a SIGKILL or your machine to reboot.

Expected behavior

Mypy should exit normally

Environment & setup

  • OS: Mac OS and ubuntu
  • Database: N/A
  • Python version: 3.6
  • Prisma version: N/A

Perform syntax check after client generation

Problem

It is possible that the generator will generate invalid python code leading to the user having to uninstall and reinstall the prisma package

Suggested solution

Either parse the rendered files with an AST parser or import them after generation, if erroneous, remove the rendered files

Package binaries in wheels as an alternative to downloading at runtime

Problem

We currently only package one universal wheel when we could make use of platform dependent wheels to improve first-time user experience, when someone installs a python package they don't expect to have to wait while more binaries are downloaded.

Suggested solution

Update setup.py wheel building to build platform dependent wheels for every platform that prisma supports, we could also fall back to building the rust binaries on the user's machine if they are on an unsupported platform but thats outside the scope of this issue.

Add support for third party concurrency libraries

Problem

Not all python frameworks use the standard library asyncio module for running coroutines, we should add first class support for such frameworks

Suggested solution

Not clear, don't have enough experience using other concurrency frameworks.

Additional context

Partial type generation does not include newly generated modules

Bug description

When using a partial type generator and a custom output directory, the partially generated package is not imported when the partial type generator is ran.

How to reproduce

schema.prisma

datasource db {
  provider = "postgres"
  url      = env("DB_URL")
}

generator client {
  provider               = "prisma-client-py"
  partial_type_generator = "partials.py"
  output                 = "my_prisma"
}

model User {
  id Int @id
  name String
}

partials.py

from prisma.models import User

On a fresh prisma installation (not generated), trying to generate the client will error.

prisma generate
Prisma schema loaded from schema.prisma
An exception ocurred while running the partial type generator
Error:
  File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
  File "partials.py", line 1, in <module>
    from prisma.models import User
ModuleNotFoundError: No module named 'prisma.models'

Expected behavior

The prisma package should point to the newly generated package.

Should be noted that in the example given above, even trying to import from my_prisma leads to an error but even if it didn't the point still stands as sometimes it may be useful to generate the client to a directory that is multiple directories away and fixing this would require knowledge of where the client was being generated to and some horrible python path patching.

Solution

I do not know how this can be solved as it is due to Python's import caching mechanism, the importlib.reload may be useful here.

In terms of how to integrate a solution into this library you will have to modify the Module.run method in src/prisma/generator/models.py.

PyPi name transfer request

Problem

The prisma package on PyPi is not maintained, we should look into transferring the project name to us.

The rules for requesting a name transfer are defined in PEP 541.

The authors of the prisma package can be found here.

Delegate binary downloading to prisma

Problem

With many possible configurations and settings, binary downloading is complicated, as such we should see if it is possible to delegate this task to the prisma as they already handle all of it for us.

This is an issue as we currently don't respect any binary targets options or http proxy options like prisma does.

We would still have to download the CLI ourselves at the very least first.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.