shopinvader / odoo-shopinvader

Odoo Modules. Sorry Magento, Shopinvader is coming
GNU Affero General Public License v3.0
121 stars 105 forks source link

shopinvader_api_address: support partial update #1443

Closed sebastienbeau closed 8 months ago

sebastienbeau commented 1 year ago

Add partial update on address (see here : https://github.com/shopinvader/odoo-shopinvader/pull/1425/files#r1361596661) @sbidoul @lmignon

sbidoul commented 1 year ago

IIRC the full update was a conscious decision in #1361, but I can't find the discussion right now.

How do you reset values to null with this approach?

What is the benefit of a partial update?

If we do this, this must be explained clearly in the service docs visible in swagger.

lmignon commented 1 year ago

@sbidoul The reset of value is easy. If a key for a field is into the json rqst, that means that a new value is provided and must be set as new value (even if null)...

In the schema definition, we can specify that a field is required and nullable. Required means that the field must be present into the json. Nullable applies to the provided value.....

    # name is optional into the request and can be null
    name: str | None = None

   # name is required into the request and can be null
    name: str | None

   # name is optional into the request but when provided can't be null
    name: str = None

   # name is required into the request but when provided can't be null
    name: str

The call to the method self.model_dump(exclude_unset=True) return a dictionary with only fields provided into the json....

# nullable and optional
class A(BaseModel):
    name: str | None = None

print(A().model_dump(exclude_unset=True))
>> {}
print(A(name=None).model_dump(exclude_unset=True))
>> {'name': None}
print(A(name="a").model_dump(exclude_unset=True))
>> {'name': 'a'}

# nullable and required
class A(BaseModel):
    name: str | None

print(A().model_dump(exclude_unset=True))
>> pydantic_core._pydantic_core.ValidationError: 1 validation error for A
>> name
>>  Field required [type=missing, input_value={}, input_type=dict]
>>    For further information visit https://errors.pydantic.dev/2.3/v/missing
print(A(name=None).model_dump(exclude_unset=True))
>> {'name': None}
print(A(name="a").model_dump(exclude_unset=True))
>> {'name': 'a'}

# not nullable and required
class A(BaseModel):
    name: str

print(A().model_dump(exclude_unset=True))
>> pydantic_core._pydantic_core.ValidationError: 1 validation error for A
>> name
>>  Field required [type=missing, input_value={}, input_type=dict]
>>    For further information visit https://errors.pydantic.dev/2.3/v/missing
print(A(name=None).model_dump(exclude_unset=True))
>> pydantic_core._pydantic_core.ValidationError: 1 validation error for A
>> name
>>  Input should be a valid string [type=string_type, input_value=None, input_type=NoneType]
>>   For further information visit https://errors.pydantic.dev/2.3/v/string_type
print(A(name="a").model_dump(exclude_unset=True))
>> {'name': 'a'}

# not nullable and optional
class A(BaseModel):
    name: str = None

print(A().model_dump(exclude_unset=True))
>> {}
print(A(name=None).model_dump(exclude_unset=True))
>> pydantic_core._pydantic_core.ValidationError: 1 validation error for A
>> name
>>  Input should be a valid string [type=string_type, input_value=None, input_type=NoneType]
>>   For further information visit https://errors.pydantic.dev/2.3/v/string_type
print(A(name="a").model_dump(exclude_unset=True))
>> {'name': 'a'}

That's being said, IMO the definition of the AddressUpdate schema is not right

class AddressUpdate(StrictExtendableBaseModel):
    """
    used to update address (res.partner)
    state and country can be name or code
    """
    name: str | None = None
    street: str | None = None
    street2: str | None = None
    zip: str | None = None
    city: str | None = None
    phone: str | None = None
    email: str | None = None
    state_id: int | None = None
    country_id: int | None = None

At least the name can't be null.

class AddressUpdate(StrictExtendableBaseModel):
    """
    used to update address (res.partner)
    state and country can be name or code
    """
    name: str  = None
    ...
sbidoul commented 1 year ago

Ok, thanks for the detailed explanation.

This still raises some questions in my head:

sbidoul commented 1 year ago

do we have use cases

Forcing the front to pass the email when updating the invoicing address may be a problem.

lmignon commented 1 year ago

Ok, thanks for the detailed explanation.

This still raises some questions in my head:

  • empty vs null vs not set may be complicated to grasp

It's true at first and could lead to error for beginners.

  • having to dump back to a JSON dict is weird, and we loose rich types when doing that (e.g. dates)

model_dump doesn't dump to JSON. It dumps python value. (model_json_dum dumps to JSON)


class B(BaseModel):
    f: float
    dt: datetime
    d: date

print(B(f="5.1", d="2023-11-03", dt="2023-11-03T11:12:12Z").model_dump(exclude_unset=True))

>> {'f': 5.1, 'dt': datetime.datetime(2023, 11, 3, 11, 12, 12, tzinfo=TzInfo(UTC)), 'd': datetime.date(2023, 11, 3)}
  • do we have use cases? is it not easier for the front to send all values always?

??

  • this is easy to break inadvertently just with subtle changes in the schema

Why? We still need to define the conversion from the schema to the value to write.

  • this would need tests and docs

For sure

sbidoul commented 1 year ago

model_dump doesn't dump to JSON. It dumps python value. (model_json_dum dumps to JSON)

Ah, TIL, thanks!

Ok then. So do we want to set this as a pattern that all shopinvader update services should work this way?

lmignon commented 1 year ago

model_dump doesn't dump to JSON. It dumps python value. (model_json_dum dumps to JSON)

Ah, TIL, thanks!

Ok then. So do we want to set this as a pattern that all shopinvader update services should work this way?

Pros:

Less:

The last point can be addressed by adopting a different pattern for processing the data received.

def to_res_partner_vals(self) -> dict:
        values = self.model_dump(exclude_unset=True)
        vals = {}
        if "name" in values:
            vals["name"] = self.name
       if "street" in values:
           vals["street"] = self.street
       ...
       return vals

Personally, I am in favour of adopting as a guideline the development of update services that allow partial updating.

ping @AnizR @qgroulard @simahawk @hparfr @marielejeune @GuillemCForgeFlow @cristiano-one

AnizR commented 1 year ago

Personally, I am in favour of adopting as a guideline the development of update services that allow partial updating.

I think that partial update can be really useful and I am not against it!

Shouldn't we make a distinction between partial update and total update?

This could be done using the POST (partial update) and the PUT (total update) method.

lmignon commented 11 months ago

/ocabot merge patch

shopinvader-git-bot commented 11 months ago

On my way to merge this fine PR! Prepared branch 16.0-ocabot-merge-pr-1443-by-lmignon-bump-patch, awaiting test results.

shopinvader-git-bot commented 11 months ago

@lmignon your merge command was aborted due to failed check(s), which you can inspect on this commit of 16.0-ocabot-merge-pr-1443-by-lmignon-bump-patch.

After fixing the problem, you can re-issue a merge command. Please refrain from merging manually as it will most probably make the target branch red.

simahawk commented 11 months ago

Personally, I am in favour of adopting as a guideline the development of update services that allow partial updating.

I agree. Had to fight we this in past implementations.

This could be done using the POST (partial update) and the PUT (total update) method.

Good point.

sbidoul commented 11 months ago

I'm investigating the test failure. Probably related to https://github.com/OCA/rest-framework/pull/402

sbidoul commented 11 months ago

Tests fixed in #1486

sebastienbeau commented 8 months ago

/ocabot merge patch

shopinvader-git-bot commented 8 months ago

Hey, thanks for contributing! Proceeding to merge this for you. Prepared branch 16.0-ocabot-merge-pr-1443-by-sebastienbeau-bump-patch, awaiting test results.

shopinvader-git-bot commented 8 months ago

It looks like something changed on 16.0 in the meantime. Let me try again (no action is required from you). Prepared branch 16.0-ocabot-merge-pr-1443-by-sebastienbeau-bump-patch, awaiting test results.

shopinvader-git-bot commented 8 months ago

Congratulations, your PR was merged at d568329d8dec6addc4be1f0af5adefd067d61423. Thanks a lot for contributing to shopinvader. ❤️