Repository navigation
Support overrides and historical values on customer, contact and attributes endpoints #145
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
Changes from all commits
a64fb20
9235255
d3e2e91
4c2354b
50db223
31952ea
84d38fc
3c62539
b3daeb8
845d2b4
09c715d
c259637
0cd94cb
33a102a
925b8a3
b05d8c2
0e4a844
289508a
3997fda
69098ba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -134,6 +134,13 @@ chartmogul.DataSource.destroy(config, uuid='ds_5915ee5a-babd-406b-b8ce-d207133fb | |
| chartmogul.Customer.create(config, data={}) | ||
| chartmogul.Customer.all(config, cursor='cursor==', per_page=20) | ||
| 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 | ||
| # attributes_with_history is a comma-separated string of case-sensitive names; | ||
| # custom attributes take the custom. prefix; unknown names return a 422 | ||
| # Overrides and history live at the top level of the customer (customer.overrides, | ||
| # customer.historical_values); customer.attributes.overrides is never populated | ||
| chartmogul.Customer.search(config, email='email@email.com') | ||
| chartmogul.Customer.merge(config, data={ | ||
| 'from': {'customer_uuid': 'cus_5915ee5a-babd-406b-b8ce-d207133fb4cb'}, | ||
|
|
@@ -150,6 +157,20 @@ chartmogul.Customer.modify(config, uuid='cus_5915ee5a-babd-406b-b8ce-d207133fb4c | |
| "country": "US", | ||
| "state": "CA", | ||
| }) | ||
| chartmogul.Customer.modify(config, uuid='cus_5915ee5a-babd-406b-b8ce-d207133fb4cb', data={ | ||
| "company": "Pinata Technologies", | ||
| "overrides": { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The examples show the payload shape, but not what the flags actually do. Could we add a sentence or two? Something like:
Without this, someone updating a pinned field will see their write silently undone.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added c259637 some short explanations 👍
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tweaked 69098ba
|
||
| "company": True, | ||
| "address": True, | ||
| "attributes": {"custom": {"channel": True}} | ||
| } | ||
| }) | ||
| # An overrides flag set to True protects the value written by the request from | ||
| # later updates; False removes an existing override and restores the field's | ||
| # most recent value from edit history. API writes to an overridden field are | ||
| # reverted unless the request carries a flag for that field. All write calls accept an overrides entry in data: | ||
| # Customer and Contact create/modify, Customer.createContact, and | ||
| # CustomAttributes add/update/remove (including add by email). | ||
| chartmogul.Customer.destroy(config, uuid='cus_5915ee5a-babd-406b-b8ce-d207133fb4cb') | ||
| chartmogul.Customer.subscriptions(config, uuid='cus_5915ee5a-babd-406b-b8ce-d207133fb4cb') | ||
| chartmogul.Customer.connectSubscriptions(config, uuid='cus_5915ee5a-babd-406b-b8ce-d207133fb4cb', data={ | ||
|
|
@@ -196,11 +217,18 @@ chartmogul.Customer.createTask(config, uuid='cus_5915ee5a-babd-406b-b8ce-d207133 | |
| chartmogul.Contact.create(config, data={}) | ||
| chartmogul.Contact.all(config, cursor='aabbcc', per_page=20, customer_uuid='cus_5915ee5a-babd-406b-b8ce-d207133fb4cb', data_source_uuid='ds_fef05d54-47b4-431b-aed2-eb6b9e545430', email='test@example.com', customer_external_id='cus_0001', external_id='con_0001') | ||
| chartmogul.Contact.retrieve(config, uuid='con_5915ee5a-babd-406b-b8ce-d207133fb4cb') | ||
| chartmogul.Contact.retrieve(config, uuid='con_5915ee5a-babd-406b-b8ce-d207133fb4cb', | ||
| with_overrides=True, attributes_with_history='title,email') | ||
| # with_overrides and attributes_with_history only work on retrieve; list endpoints ignore them | ||
| chartmogul.Contact.merge(config, into_uuid='con_5915ee5a-babd-406b-b8ce-d207133fb4cb', from_uuid='con_2123290f-09c8-4628-a205-db5596bd58f7') | ||
| chartmogul.Contact.modify(config, uuid='con_5915ee5a-babd-406b-b8ce-d207133fb4cb', data={ | ||
| "email": "test@example.com", | ||
| "last_seen": "2025-04-01T12:00:00Z" | ||
| }) | ||
| chartmogul.Contact.modify(config, uuid='con_5915ee5a-babd-406b-b8ce-d207133fb4cb', data={ | ||
| "title": "CTO", | ||
| "overrides": {"title": True} | ||
| }) | ||
| chartmogul.Contact.destroy(config, uuid='con_5915ee5a-babd-406b-b8ce-d207133fb4cb') | ||
| chartmogul.Contact.tasks(config, uuid='con_5915ee5a-babd-406b-b8ce-d207133fb4cb', cursor='aabbcc', per_page=20) | ||
| chartmogul.Contact.createTask(config, uuid='con_5915ee5a-babd-406b-b8ce-d207133fb4cb', data={}) | ||
|
|
@@ -290,10 +318,14 @@ Date filters take ISO 8601 strings. `Task.patch` returns `None` when the API rep | |
|
|
||
| #### [Customer Attributes](https://dev.chartmogul.com/reference/customers/attributes/) | ||
|
|
||
| Note that the returned attributes of type date are not parsed and stay in string. | ||
| Note that the returned attributes of type date are not parsed and stay in string. The same applies inside `historical_values` entries: `update_performed_at` stays an ISO 8601 string. | ||
|
|
||
| ```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') | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Historical value entries come back as plain dicts, so
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done 0e4a844 |
||
| # attributes_with_history accepts custom attributes only on this endpoint; | ||
| # standard field names like 'company' return a 422 | ||
| ``` | ||
|
|
||
| #### [Tags](https://dev.chartmogul.com/reference/customers/tags/) | ||
|
|
@@ -325,6 +357,15 @@ chartmogul.CustomAttributes.add(config, data={ | |
| {'type': 'Integer', 'key': 'age', 'value': 8} | ||
| ] | ||
| }) | ||
| chartmogul.CustomAttributes.add(config, uuid='cus_5915ee5a-babd-406b-b8ce-d207133fb4cb', data={ | ||
| 'custom': [ | ||
| {'type': 'String', 'key': 'channel', 'value': 'Facebook'} | ||
| ], | ||
| 'overrides': {'custom': {'channel': True}} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Worth calling out that the
The examples are correct, but it's easy to copy the wrong one between sections. A one-line note would help.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added note 0cd94cb 👍 |
||
| }) | ||
| # Note the overrides shape here is {'custom': {...}} — on Customer create/modify | ||
| # and in Customer.retrieve responses the same flags nest under 'attributes': | ||
| # {'attributes': {'custom': {...}}} | ||
| chartmogul.CustomAttributes.update(config, uuid='cus_5915ee5a-babd-406b-b8ce-d207133fb4cb', data={ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The API also accepts
Either an example for each, or a single line saying "all create/modify/update/add/remove calls accept an
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Went with the one-liner b05d8c2 👍 |
||
| 'custom': { | ||
| 'age': 20, | ||
|
|
@@ -334,6 +375,12 @@ chartmogul.CustomAttributes.update(config, uuid='cus_5915ee5a-babd-406b-b8ce-d20 | |
| chartmogul.CustomAttributes.remove(config, uuid='cus_5915ee5a-babd-406b-b8ce-d207133fb4cb', data={ | ||
| 'custom': ['CAC'] | ||
| }) | ||
| chartmogul.CustomAttributes.remove(config, uuid='cus_5915ee5a-babd-406b-b8ce-d207133fb4cb', data={ | ||
| 'custom': ['CAC'], | ||
| 'overrides': {'custom': {'CAC': False}} | ||
| }) | ||
| # remove keeps overridden attributes unless the request carries a False flag | ||
| # for them; the response message names the ones kept. | ||
| ``` | ||
|
|
||
| #### [Plans](https://dev.chartmogul.com/reference/plans/) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,12 +8,17 @@ class Attributes(Resource): | |
| """ | ||
|
|
||
| _path = "/customers{/uuid}/attributes" | ||
| _bool_query_params = ["with_overrides"] | ||
|
|
||
| class _Schema(Schema): | ||
| tags = fields.List(fields.String()) | ||
| stripe = fields.Dict() | ||
| clearbit = fields.Dict() | ||
| 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) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I don't think it's worth a separate nested schema, but a README note would stop people reaching for
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hope 33a102a is good enough. 👍 |
||
| historical_values = fields.Dict(load_default=None) | ||
|
|
||
| @post_load | ||
| def make(self, data, **kwargs): | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| import unittest | ||
| from chartmogul import Attributes, Config | ||
| import requests_mock | ||
|
|
||
| attributesResponse = { | ||
| "tags": ["engage", "unit loss"], | ||
| "stripe": {"uid": 7, "coupon": True}, | ||
| "custom": {"CAC": 213, "salesRep": "Gabi"}, | ||
| "overrides": {"custom": {"salesRep": True}}, | ||
| "historical_values": { | ||
| "custom": { | ||
| "salesRep": [ | ||
| { | ||
| "value": "Gabi", | ||
| "update_performed_at": "2026-01-01T16:58:58Z", | ||
| "update_performed_by": "API", | ||
| "initial": False, | ||
| } | ||
| ] | ||
| } | ||
| }, | ||
| } | ||
|
|
||
|
|
||
| class AttributesTestCase(unittest.TestCase): | ||
| @requests_mock.mock() | ||
| def test_retrieve(self, mock_requests): | ||
| plainResponse = {key: attributesResponse[key] for key in ("tags", "stripe", "custom")} | ||
| mock_requests.register_uri( | ||
| "GET", | ||
| "https://api.chartmogul.com/v1/customers/CUSTOMER_UUID/attributes", | ||
| status_code=200, | ||
| json=plainResponse, | ||
| ) | ||
|
|
||
| config = Config("token") | ||
| result = Attributes.retrieve(config, uuid="CUSTOMER_UUID").get() | ||
|
|
||
| self.assertEqual(mock_requests.call_count, 1, "expected call") | ||
| self.assertEqual(mock_requests.last_request.qs, {}) | ||
| self.assertTrue(isinstance(result, Attributes)) | ||
| self.assertEqual(result.custom, attributesResponse["custom"]) | ||
| self.assertIsNone(result.overrides) | ||
| self.assertIsNone(result.historical_values) | ||
|
|
||
| # case_sensitive asserts that the SDK preserves attribute name casing in the query | ||
| @requests_mock.Mocker(case_sensitive=True) | ||
| def test_retrieve_with_overrides_and_history(self, mock_requests): | ||
| mock_requests.register_uri( | ||
| "GET", | ||
| "https://api.chartmogul.com/v1/customers/CUSTOMER_UUID/attributes", | ||
| status_code=200, | ||
| json=attributesResponse, | ||
| ) | ||
|
|
||
| config = Config("token") | ||
| result = Attributes.retrieve( | ||
| config, | ||
| uuid="CUSTOMER_UUID", | ||
| with_overrides=True, | ||
| attributes_with_history="custom.salesRep", | ||
| ).get() | ||
|
|
||
| self.assertEqual(mock_requests.call_count, 1, "expected call") | ||
| self.assertEqual( | ||
| mock_requests.last_request.qs, | ||
| {"with_overrides": ["true"], "attributes_with_history": ["custom.salesRep"]}, | ||
| ) | ||
| self.assertTrue(isinstance(result, Attributes)) | ||
| self.assertEqual(result.custom, attributesResponse["custom"]) | ||
| self.assertEqual(result.overrides, attributesResponse["overrides"]) | ||
| self.assertEqual(result.historical_values, attributesResponse["historical_values"]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we document what format
attributes_with_historytakes?custom.prefix.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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hope 925b8a3 makes sense, I kept the 100 cap hidden from dev docs so I'll do the same here.