uplink icon indicating copy to clipboard operation
uplink copied to clipboard

Support *args **kwargs to built in converters (e.g. pydantic by_alias)

Open jkornblum opened this issue 4 years ago • 4 comments

Is your feature request related to a problem? Please describe. Pydantic is supported out of the box but I can't use it to serialize when using field aliases. Pydantic has support for this but I don't see a way to pass this information in Uplink.

Say I have a pydantic model like the following. The API uses camelcase but Python api should use snake case.

from pydantic import BaseModel

def to_camel(string: str) -> str:
    return "".join(word.capitalize() for word in string.split("_"))

class Case(BaseModel):
  instance_name: str
  instance_state: str

    class Config:
        alias_generator = to_camel
        allow_population_by_field_name = True

In Uplink I have

from .models import Case
from uplink import Consumer, get, post, json, returns, Body

class CaseAPI(Consumer):
    @get("scenario/{case_id}")
    def get(self, case_id: int) -> Case:
        """Get case by case_id"""

    @json
    @returns.json
    @post("scenario")
    def create(self, payload: Body(type=Case)):
        """Create a new case"""

The GET works based on the alias_generator in model but the POST does not work (does not convert snake to camel case) because one needs to pass by_alias argument to the .json() method on Pydantic model class [doc]

Describe the solution you'd like I'd like to be able to pass *args and **kwargs to the converters that are built in. API something like

...
    @json
    @returns.json
    @post("scenario")
    def create(self, payload: Body(type=Case, by_alias=True)):
....

For now my work-around is just to use custom serialization function.

Cheers - Josh

jkornblum avatar Jun 08 '21 17:06 jkornblum

I just hit this too

it seems like uplink does not call pydantic_model.dict() like you would expect, but instead uses this other function: https://github.com/prkumar/uplink/blob/d3ee283426873ad61243b5759b7cb52f8f712567/uplink/converters/pydantic_.py#L12

and in pydantic that method doesn't pass thru kwargs to dict() https://github.com/samuelcolvin/pydantic/blob/6f46a5a1460728868f860840e3f6186aa40403d8/pydantic/json.py#L78

 return obj.dict()

anentropic avatar Jan 11 '22 17:01 anentropic

I'm looking into different approaches for how we can address this. The approach I'm favoring presently is to define a new decorator (e.g., converters.pydantic.dict_args(...)):

@json
@returns.json
@converters.pydantic.dict(by_alias=True)
@post("scenario")
def create(self, payload: Body(type=Case)):
	pass

Passing args through Body sounds fine to me, too. However, It may require more effort for similar functionality. One upside of this approach is that we could clearly specify different model.dict args per Body argument. However, I don't think such a feature would be necessary, since there should only be one @Body-annotated argument per @json-decorated method.

prkumar avatar Jan 16 '22 01:01 prkumar

As long as the by_alias works by really providing aliases requested here. I remember some surprising name effect collisions fro browsing through the pydantic project issue discussions after banging my head trying to hand-over snake case aliases as function arguments onto camel case model elements when in reality this was only working for pushing dicts/json through the parameter gate. But maybe that is exactly the use case here. I do not remember the name Samuel Colvin in retrospect would have preferred for the concept - I will try to find it again and update my words here, sorry for the noise.

Update: Here is my homework 🙈 https://github.com/samuelcolvin/pydantic/issues/565

sthagen avatar Jan 16 '22 08:01 sthagen

@jkornblum How were you able to override the built-in Pydantic converter? I've created a custom converter, but it doesn't seem to take precedence.

edit: Ah, nevermind. I was missing the @json decorator.

mure avatar Oct 03 '22 17:10 mure