mooncell07 / Azaka

Python Wrapper for the VNDB REST API.
https://mooncell07.github.io/Azaka/
MIT License
3 stars 0 forks source link

[Doc request] Example of using filters with boolean operators #16

Closed oniietzschan closed 1 month ago

oniietzschan commented 2 months ago

I figured out that you can write something like this:

import asyncio
from azaka import Client, Node, select

query = (
    select(
        "title",
        "platforms",
        "languages",
        "image.url"
    )
    .frm("vn")
    .where([
        'and',
        ['lang', '=', 'en'],
        ['platform', '=', 'p98'],
    ])
)

async def main() -> None:
    async with Client() as client:
        resp = await client.execute(query=query)
        vn = resp.results[0]
        print(vn)

asyncio.run(main())

I'm not sure if there's a better way to accomplish this that makes use of the Node construct?

Anyways, I'm just suggesting that you add a example to the docs that represents a best practice for using AND / OR. Otherwise using this library has been pretty intuitive so far.

oniietzschan commented 2 months ago

Oh, I kind of figured it out myself. Someone else might find elements of this snippet useful:

import asyncio
from azaka import AND, Client, Node, OR, select, Paginator

query = (
    select(
        'title',
        'platforms',
        'languages'
    )
    .frm('vn')
    .where(AND(
        Node('lang') == 'en',
        Node('platform') == 'p98'
    ))
    .sort('title')
)

async def main() -> None:
    async with Client() as client:
        pages = Paginator(client, query, max_results_per_page=50)
        while await pages.next():
            resp = pages.current()
            for vn in resp.results:
                print(vn.id, vn.title)

asyncio.run(main())
mooncell07 commented 2 months ago

Hi! Thanks for using Azaka! Yeah I have done a horrible job at documenting stuff :( I will try my best to get everything documented as soon as possible. Do you have any more issues?

oniietzschan commented 2 months ago

Hm, the one other mildly-unintuitive thing I came across was the syntax for filtering on related items, ex:

def find_releases_for_vn(client: Client, vn_id: str):
    query = (
        select()
        .frm('release')
        .where(Node('vn') == (Node('id') == vn_id))
    )
    return client.execute(query=query)

To be perfectly honest, it feel it might be easier for people to understand if you just preferred a style where you passed in what literally ends up being in the query, ex.

query = (
    select()
    .frm('release')
    .where(['vn', '=', ['id', '=', vn_id]])
)

Once I understood that Node() was basically syntactic sugar for that, it was easy enough to understand. But at the start it was a bit of a barrier to understanding how to go from the VNDB docs (https://api.vndb.org/kana#filters) to what your library seemed to prefer. Just my 2 cents -- but maybe there's some value to Node() that hasn't clicked for me yet.

mooncell07 commented 2 months ago

Yes Node is there for some simple abstraction over the low level encoding. I decided to abstract it away because i felt that writing all those nested lists by hand is error prone and hard to debug especially when queries are big and complex. However yeah you can pass your filters as lists to the where function.

querya = (
    select("id", "average")
    .frm("vn")
    .where(
        ["and",
            ["or", 
                ["lang", "=", "en"],
                ["lang", "=", "de"], 
                ["lang", "=", "fr"]
            ],

            ["olang", "!=", "ja"],
            ["release", "=", ["and",
                    ["released", ">=", "2020-01-01"],
                    ["producer", "=", ["id", "=", "p30"]],
                ],
            ],
        ]
    )
)

Node comes in handy when you have to build giant filters such as the one above

queryb = (
    select("id", "average")
    .frm("vn")
    .where(
        AND(
            OR(
                Node("lang") == "en", 
                Node("lang") == "de",
                Node("lang") == "fr"
            ),

            Node("olang") != "ja",

            Node("release") == AND(
                Node("released") >= "2020-01-01",
                Node("producer") == (Node("id") == "p30"),
            ),
        )
    )
)

I think the latter is more clear and easier to build in general.

One more thing, though really silly but you can alias Node to lets say Release to make it look even more clear and specific

    query = (
        select()
        .frm('release')
        .where(Release('vn') == (Release('id') == vn_id))
    )

I do understand that due to the nature of this library, users are required to constantly cross-reference VNDB API and adding my own abstraction layer over anything can cause alot of confusion so i will try to use the list approach over Node approach in documentation and examples and introduce the concept of Node as an extension separately.

Anyways to finish this, yeah i have plans to extend Node, such as i am thinking of using in operator to flatten out OR chains

OR( Node("lang") == "en", Node("lang") == "de", Node("lang") == "fr" ) to Node("lang") in ["en", "de", "fr"]