Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

shopinvader_api_address: support partial update #1443

Merged

Conversation

sebastienbeau
Copy link
Contributor

@sbidoul
Copy link
Member

sbidoul commented Nov 3, 2023

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
Copy link
Collaborator

lmignon commented Nov 3, 2023

@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
Copy link
Member

sbidoul commented Nov 3, 2023

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
  • having to dump back to a JSON dict is weird, and we loose rich types when doing that (e.g. dates)
  • 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
  • this would need tests and docs

@sbidoul
Copy link
Member

sbidoul commented Nov 3, 2023

do we have use cases

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

@lmignon
Copy link
Collaborator

lmignon commented Nov 3, 2023

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
Copy link
Member

sbidoul commented Nov 3, 2023

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
Copy link
Collaborator

lmignon commented Nov 3, 2023

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 constraining for client applications -> add more flexibility in the way to design the updates on service consumer

Less:

  • Requires greater skills in the advanced features offered by Pydantic.
  • Less explicit and less traceable approach of the use of attributes defined by the schema

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
Copy link
Contributor

AnizR commented Nov 4, 2023

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
Copy link
Collaborator

lmignon commented Dec 14, 2023

/ocabot merge patch

@shopinvader-git-bot
Copy link

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 pushed a commit that referenced this pull request Dec 14, 2023
Signed-off-by lmignon
@shopinvader-git-bot
Copy link

@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
Copy link
Contributor

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
Copy link
Member

sbidoul commented Dec 14, 2023

I'm investigating the test failure. Probably related to OCA/rest-framework#402

@sbidoul
Copy link
Member

sbidoul commented Dec 14, 2023

Tests fixed in #1486

@sebastienbeau
Copy link
Contributor Author

/ocabot merge patch

@shopinvader-git-bot
Copy link

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 pushed a commit that referenced this pull request Mar 26, 2024
Signed-off-by sebastienbeau
@shopinvader-git-bot
Copy link

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 shopinvader-git-bot merged commit 4c00f2e into shopinvader:16.0 Mar 26, 2024
3 checks passed
@shopinvader-git-bot
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants