Repository navigation
Support overrides and historical values on customer, contact and attributes endpoints - #145
Conversation
keith-chartmogul
left a comment
There was a problem hiding this comment.
I'm not a Python developer, so Claude assisted with the heavy lifting on this review. There were a few points raised during the review, which all seem legitimate to my untrained eye, so I posted inline comments with those findings. Please feel free to ignore if they do not apply, but I want to make sure you see them and consider them before choosing to ignore or act on any of them. 👍
…sponse keys do not raise AttributeError
…se AttributeError
7cafdef to
31952ea
Compare
keith-chartmogul
left a comment
There was a problem hiding this comment.
Some more findings from another review round. All testing and documentation related, but I think these are all probably worth doing ahead of the release. 💪
| self.assertEqual(mock_requests.call_count, 1, "expected call") | ||
| self.assertEqual(mock_requests.last_request.qs, {}) | ||
| self.assertEqual(mock_requests.last_request.json(), sentCreateExpected) | ||
| self.assertIsNone(result.overrides) |
There was a problem hiding this comment.
Also applies to line 107 in test/api/test_custom_attrs.py
This now asserts overrides is None on a create response, but POST /customers always returns overrides (it renders with with_overrides: true). The same goes for test_custom_attrs.py:107: POST .../attributes/custom always includes overrides in its response. So both assertions pin a response shape the API can't produce, and they contradict the PR description ("write responses always include overrides").
Could we add overrides to the mocked responses for these two tests, and move the "missing key → None" check to a plain Customer.retrieve test? test_customer.py doesn't have one yet; only test_retrieve_with_overrides_and_history exists. Small related thing: test_create still mocks a 200, while the new test_create_with_overrides correctly uses 201.
There was a problem hiding this comment.
The PR description mentions that the add-by-email response picks up overrides through Customer._Schema, and the API does return overrides for each customer in that response. But this test's fixture (jsonResponse2) has no overrides and the test makes no assertions on the result, so nothing actually covers it.
Also, this calls CustomAttributes.add with uuid="CUSTOMER_UUID", so it hits the single-customer endpoint rather than the by-email one (/customers/attributes/custom). That isn't new to this PR, but since we're relying on this path now, could we call it without uuid (or via add_by_email), add overrides to the fixture entries, and assert result.entries[0].overrides?
| self.assertEqual(result.historical_values, historical_values) | ||
|
|
||
| @requests_mock.mock() | ||
| def test_create_with_overrides(self, mock_requests): |
There was a problem hiding this comment.
Nice. Could we add a matching test for Customer.modify with overrides? It's the main example in the README, and right now only create is tested.
The API also accepts overrides on two other writes that have no tests:
Contact.create/Customer.createContactCustomAttributes.update(PUT)
They all serialize the body the same way, so the risk is low. A short body-pinning test for each would still back up the README, for example:
self.assertEqual(mock_requests.last_request.json(), jsonRequest)
self.assertEqual(result.overrides, overrides)| self.assertEqual(mock_requests.last_request.qs, {}) | ||
| self.assertTrue(expected, {}) | ||
|
|
||
| # case_sensitive pins that the SDK does not alter query param casing |
There was a problem hiding this comment.
Nit: title is all lowercase, so case_sensitive=True and the comment above it don't prove anything here; the test would pass either way. I'd drop the comment and use the default @requests_mock.mock(), unless we use a mixed-case value.
There was a problem hiding this comment.
Right 09c715d — with all-lowercase contact field names it discriminates nothing, and a mixed-case value would pin a shape the contact endpoint rejects (field names match case-sensitively, so Title 422s). Reverted to the default mocker; the customer and attributes tests keep case_sensitive=True where salesRep makes it meaningful.
| }) | ||
| chartmogul.Customer.modify(config, uuid='cus_5915ee5a-babd-406b-b8ce-d207133fb4cb', data={ | ||
| "company": "Pinata Technologies", | ||
| "overrides": { |
There was a problem hiding this comment.
The examples show the payload shape, but not what the flags actually do. Could we add a sentence or two? Something like:
Truepins the field's value after the write.Falseunpins it and restores the most recent value from edit history.- While a field is pinned, API writes to it are reverted unless the request includes an override for that field.
CustomAttributes.removeskips pinned attributes; the response'smessagesays which ones were kept.
Without this, someone updating a pinned field will see their write silently undone.
There was a problem hiding this comment.
Tweaked 69098ba
Correction to my earlier reply: you were right about the restore.
UnpinService#unpin_field!ends withrestore_most_recent_value!— blocked writes still land in edit history, so removing the override catches the field up to them. Our live check couldn't distinguish this because the pinned value was also the latest history entry. The README note is fixed accordingly.
| custom = fields.Dict() | ||
| # load_default=None ensures these attributes are always present; the API | ||
| # omits the keys unless with_overrides / attributes_with_history is passed | ||
| overrides = fields.Dict(load_default=None) |
There was a problem hiding this comment.
Customer._Schema.attributes reuses this schema for its nested attributes field, so a Customer now always has customer.attributes.overrides and customer.attributes.historical_values, both None. On a customer, custom-attribute pins and history actually live in customer.overrides["attributes"] and customer.historical_values["attributes"], so these two will never be filled in.
I don't think it's worth a separate nested schema, but a README note would stop people reaching for customer.attributes.overrides.
| chartmogul.Customer.retrieve(config, uuid='cus_5915ee5a-babd-406b-b8ce-d207133fb4cb') | ||
| chartmogul.Customer.retrieve(config, uuid='cus_5915ee5a-babd-406b-b8ce-d207133fb4cb', | ||
| with_overrides=True, attributes_with_history='company,custom.channel') | ||
| # with_overrides and attributes_with_history only work on retrieve; list and search endpoints ignore them |
There was a problem hiding this comment.
Could we document what format attributes_with_history takes?
- It's a comma-separated string, and names are case-sensitive.
- Custom attributes need the
custom.prefix. - At most 100 names; unknown names return a 422.
Attributes.retrieveonly accepts custom attributes, soattributes_with_history='company'there returns a 422.
I'd keep it a string rather than also accepting a list: the Ruby, Node, PHP and Go SDKs all take a comma-separated string.
There was a problem hiding this comment.
Hope 925b8a3 makes sense, I kept the 100 cap hidden from dev docs so I'll do the same here.
| ], | ||
| 'overrides': {'custom': {'channel': True}} | ||
| }) | ||
| chartmogul.CustomAttributes.update(config, uuid='cus_5915ee5a-babd-406b-b8ce-d207133fb4cb', data={ |
There was a problem hiding this comment.
The API also accepts overrides on these writes, but the README has no example for them:
Customer.createContact.create/Customer.createContactCustomAttributes.updateCustomAttributes.addby email
Either an example for each, or a single line saying "all create/modify/update/add/remove calls accept an overrides dict", would cover it.
| ```python | ||
| chartmogul.Attributes.retrieve(config, uuid='cus_5915ee5a-babd-406b-b8ce-d207133fb4cb') | ||
| chartmogul.Attributes.retrieve(config, uuid='cus_5915ee5a-babd-406b-b8ce-d207133fb4cb', | ||
| with_overrides=True, attributes_with_history='custom.channel') |
There was a problem hiding this comment.
Historical value entries come back as plain dicts, so update_performed_at stays an ISO 8601 string and isn't converted to a datetime, unlike other datetime fields in the SDK. Since this section already notes that date attributes stay as strings, could we extend that note to cover historical_values?
| address = fields.Nested(Address._Schema, allow_none=True, unknown=EXCLUDE) | ||
| website_url = fields.String(allow_none=True) | ||
| # load_default=None ensures these attributes are always present; the API | ||
| # omits the keys unless with_overrides / attributes_with_history is passed |
There was a problem hiding this comment.
Also applies to line 38 in chartmogul/api/contact.py
Small accuracy fix: write responses (create/modify) always include overrides, so "omits the keys unless with_overrides / attributes_with_history is passed" only holds for retrieve. Maybe "on retrieve, the API omits these keys unless…"? The same wording is in contact.py:38. It's fine as is in attributes.py, since that endpoint is GET-only.
… to a plain retrieve test
…names are lowercase
Adds support for the overrides and historical values API to the Python client, targeting the current API where write responses always include
overrides.Customer,ContactandAttributesregisterwith_overridesin_bool_query_paramssowith_overrides=Trueserializes astrue(same pattern aswith_processing_statusonDataSource);attributes_with_historypasses through as a plain string query param.unknown=EXCLUDE, sooverridesandhistorical_valueswere dropped from responses;fields.Dict(load_default=None)entries on theCustomer,ContactandAttributesschemas now keep them and make the attributes always present (Nonewhen the API omits the key), with attribute names round-tripping verbatim (Dict fields don't convert keys). TheCustomAttributesschema keepsoverrides,custom(absent when a DELETE removes the last attribute) and the DELETEmessagethe same way; the add-by-email response loads throughCustomer._Schemaand picks upoverridesfrom there.datadicts are dumped as-is, sooverridesalready reaches the API on every write; tests pin the outgoing bodies (last_request.json()) and query strings.overrides(distinguishing{}fromNone), mock statuses match the API (201 on creates, 202 on deletes), the by-email test exercises the real/customers/attributes/customendpoint and asserts entry overrides, and the with-history tests use case-sensitive request mocks where attribute names are mixed-case (the API matches names case-sensitively).overrides, plus notes on flag semantics, which write calls acceptoverrides, the payload shape difference between the customer and custom attribute endpoints, theattributes_with_historyformat, that overrides live at the top level of the customer, and that historical value timestamps stay as strings.