felix-martel / pydanclick

Add click options from a Pydantic model
https://felix-martel.github.io/pydanclick/
MIT License
29 stars 7 forks source link

Multiple sub-models #20

Open azmeuk opened 2 months ago

azmeuk commented 2 months ago

I am well aware that this is a difficult question, but I would love if there could be a clean API for lists of sub-models instead of having to pass JSON.

class Sub(BaseModel):
   foo: str | None = None
   bar: str | None = None

class Super(BaseModel):
    subs: list[Sub] = []

With such models I can think of two possible ways to handle this:

What do you think?

felix-martel commented 2 months ago

I'm afraid we're reaching the limit of what the library can do. I think the syntax you're proposing adds complexity for a relatively narrow use case (as soon as the list grows, you're better off using JSON or another approach anyway). Maybe you have some real-world use case you can share so I can think of alternative approaches?

azmeuk commented 2 months ago

My use-case is that I am considering to use pydanclick in scim2-cli. In short, scim2-cli just takes input from the users, build HTTP payload and send them to a given server. I handles common objects defined by the SCIM RFCs (in particular RFC7643), such as User or Group.

In particular, User.emails is a list of Email objects. I would love a way to create an user with several email addresses with something simpler than:

scim create user --user-name "foobar" --emails '[{"value":"foo@bar.example", "primary": true}, {"value": "foo@baz.example"}]'

However, I understand the difficulty here.

azmeuk commented 2 months ago

I just thought of another syntax. I think this is the neatest I can think of:

scim create user --user-name "foobar" \
    --emails --emails-value "foo@bar.example" --primary "true" \
    --emails --emails-value "foo@baz.example"

Here --emails would just mark the creation of a new Email object, filled by the following --emails-* parameters, until a new --emails is met.

If something like this is ok with you, I would volunteer for working on it. What do you think?

felix-martel commented 2 months ago

I've drafted a solution to your problem here: #26

It can quickly become complex when you start nesting models, or using list of unions, or deeply nested lists, but I'm confident we'll be able to support basic use cases. Not sure how much time I'll have to work on it until next week end, though.

azmeuk commented 1 week ago

@felix-martel I just had a quick look at your patch. This works for regular fields but it stops working if a field is optional:

import click
from typing import Optional
from pydantic import BaseModel
from pydanclick import from_pydantic

class Author(BaseModel):
    name: str
    primary: bool = False

class Book(BaseModel):
    title: str
    authors: Optional[list[Author]]

@click.command()
@from_pydantic(Book, unpack_list=True)
def cli(book: Book):
    pass

if __name__ == "__main__":
    cli()