Skip to content

Add EntityNote resource and align Contacts and Tasks with the public API - #144

Merged
keith-chartmogul merged 2 commits into
mainfrom
align_changes_to_contacts_tasks_notes_in_public_api
Oct 12, 2026
Merged

keith-chartmogul merged 2 commits into
mainfrom
align_changes_to_contacts_tasks_notes_in_public_api

Conversation

@keith-chartmogul

@keith-chartmogul keith-chartmogul commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Aligns the SDK with recent public v1 API changes to Contacts, Tasks and Notes. Additive only; no breaking changes.

Notes

  • New EntityNote resource for /v1/notes, covering notes and call logs on customers and contacts. Exposes associated_object / associated_object_uuid and a nullable customer_uuid.
  • CustomerNote and Customer.notes / Customer.createNote are deprecated. They keep their endpoint and behavior but now emit a DeprecationWarning pointing at the EntityNote equivalent.
  • New helpers: Customer.entityNotes / Customer.createEntityNote.

Tasks

  • Task.all supports contact_uuid, assignee, due_date_on_or_after, due_date_on_or_before and completed. completed=False is sent as completed=false.
  • Task.create accepts associated_object_identifier; customer_uuid is now nullable in responses. associated_object / associated_object_uuid added to the schema.

Contacts

  • Contact.all supports email, customer_external_id and external_id filters.
  • last_seen added to the schema and accepted on create and modify. Contacts can be created without customer_uuid / data_source_uuid.
  • New helpers: Contact.tasks, Contact.createTask, Contact.entityNotes, Contact.createEntityNote. The create helpers inject an associated_object_identifier for the contact unless the payload already contains customer_uuid or an identifier.

Core

  • Resource._load returns None for HTTP 304, which PATCH /notes and PATCH /tasks return when nothing was updated.
  • Resource._method gains useAssociatedObjectFor, alongside the existing useUUIDFor.
  • _deprecated_method helper reuses the list_imported deprecation approach.

Tests and docs

  • New test_entity_note.py; new cases in the task, contact, customer, customer-note and common test modules, including 304 handling, boolean filters, standalone contacts and identifier injection.
  • README updated for Contacts, Notes, Customer Notes (deprecated), Tasks and the Customer helpers.

@keith-chartmogul
keith-chartmogul requested review from a team, briwa, tudorpavel and ya-petrov-evgeniy and removed request for a team September 16, 2026 09:39
@ya-petrov-evgeniy

Copy link
Copy Markdown

Many changes there with some mocks in tests. Did you try to test it locally in devcontainer api and some python app with our lib? This will be e2e test, maybe it finds some issues.

@keith-chartmogul

Copy link
Copy Markdown
Contributor Author

Many changes there with some mocks in tests. Did you try to test it locally in devcontainer api and some python app with our lib? This will be e2e test, maybe it finds some issues.

I don't use the platform devcontainer locally for development, so definitely did not do that. I've only run the existing test suite as it currently exists in the repo. I'll see about running the tests in a more realistic e2e way for better verification. 🫡

@keith-chartmogul

Copy link
Copy Markdown
Contributor Author

Many changes there with some mocks in tests. Did you try to test it locally in devcontainer api and some python app with our lib? This will be e2e test, maybe it finds some issues.

@ya-petrov-evgeniy Alright, I generated an e2e smoke script using this branch and ran it against the app running locally with all of the public API changes applied and the feature flag associated with this project enabled. It covered all of the new and updated code paths in this branch and passed with 100% success. ✅

@ya-petrov-evgeniy

Copy link
Copy Markdown

Nice, It requires contact_profiles feature flag, it should be fully rolled out, right?

@ya-petrov-evgeniy

Copy link
Copy Markdown

Also ran e2e tests, works as expected:


  ┌─────┬──────────────────────────────────────────────┬────────────────────────────────────────────────┬──────┬───────────────────────────────────────────┐
  │  #  │                Python method                 │                  Route called                  │ HTTP │                  Result                   │
  ├─────┼──────────────────────────────────────────────┼────────────────────────────────────────────────┼──────┼───────────────────────────────────────────┤
  │ 1   │ Task.create(data={…, customer_uuid})         │ POST /tasks                                    │ 201  │ ✅ created, customer_uuid set             │
  ├─────┼──────────────────────────────────────────────┼────────────────────────────────────────────────┼──────┼───────────────────────────────────────────┤
  │ 2   │ Task.all(customer_uuid=…)                    │ GET /tasks?customer_uuid=…                     │ 200  │ ✅ 1 task                                 │
  ├─────┼──────────────────────────────────────────────┼────────────────────────────────────────────────┼──────┼───────────────────────────────────────────┤
  │ 3   │ Task.all(…, completed=False)                 │ GET /tasks?…&completed=false                   │ 200  │ ✅ bool→false coercion works              │
  ├─────┼──────────────────────────────────────────────┼────────────────────────────────────────────────┼──────┼───────────────────────────────────────────┤
  │ 4   │ Task.all(…, assignee, due_date_on_or_after)  │ GET /tasks?…&assignee=…&due_date_on_or_after=… │ 200  │ ✅ new filters accepted                   │
  ├─────┼──────────────────────────────────────────────┼────────────────────────────────────────────────┼──────┼───────────────────────────────────────────┤
  │ 5   │ Task.retrieve(uuid)                          │ GET /tasks/{uuid}                              │ 200  │ ✅                                        │
  ├─────┼──────────────────────────────────────────────┼────────────────────────────────────────────────┼──────┼───────────────────────────────────────────┤
  │ 6   │ Task.modify(uuid, data)                      │ PATCH /tasks/{uuid}                            │ 200  │ ✅ updated                                │
  ├─────┼──────────────────────────────────────────────┼────────────────────────────────────────────────┼──────┼───────────────────────────────────────────┤
  │ 7   │ Task.patch(uuid, data={})                    │ PATCH /tasks/{uuid}                            │ 304  │ ✅ returns None (PR's 304 change)         │
  ├─────┼──────────────────────────────────────────────┼────────────────────────────────────────────────┼──────┼───────────────────────────────────────────┤
  │ 8   │ EntityNote.create(data={customer_uuid,…})    │ POST /notes                                    │ 201  │ ✅ assoc_obj=customer                     │
  ├─────┼──────────────────────────────────────────────┼────────────────────────────────────────────────┼──────┼───────────────────────────────────────────┤
  │ 9   │ EntityNote.all(customer_uuid=…)              │ GET /notes?customer_uuid=…                     │ 200  │ ✅ cursor/has_more present                │
  ├─────┼──────────────────────────────────────────────┼────────────────────────────────────────────────┼──────┼───────────────────────────────────────────┤
  │ 10  │ EntityNote.retrieve(uuid)                    │ GET /notes/{uuid}                              │ 200  │ ✅                                        │
  ├─────┼──────────────────────────────────────────────┼────────────────────────────────────────────────┼──────┼───────────────────────────────────────────┤
  │ 11  │ EntityNote.modify(uuid, data)                │ PATCH /notes/{uuid}                            │ 200  │ ✅ updated                                │
  ├─────┼──────────────────────────────────────────────┼────────────────────────────────────────────────┼──────┼───────────────────────────────────────────┤
  │ 12  │ Customer.entityNotes(uuid)                   │ GET /notes?customer_uuid=…                     │ 200  │ ✅                                        │
  ├─────┼──────────────────────────────────────────────┼────────────────────────────────────────────────┼──────┼───────────────────────────────────────────┤
  │ 13  │ Customer.createEntityNote(uuid, data)        │ POST /notes                                    │ 201  │ ✅ useUUIDFor injects customer_uuid       │
  ├─────┼──────────────────────────────────────────────┼────────────────────────────────────────────────┼──────┼───────────────────────────────────────────┤
  │ 14  │ Customer.notes(uuid) (deprecated)            │ GET /customer_notes?customer_uuid=…            │ 200  │ ✅ DeprecationWarning emitted             │
  ├─────┼──────────────────────────────────────────────┼────────────────────────────────────────────────┼──────┼───────────────────────────────────────────┤
  │ 15  │ Customer.createNote(uuid, data) (deprecated) │ POST /customer_notes                           │ 201  │ ✅ DeprecationWarning emitted             │
  ├─────┼──────────────────────────────────────────────┼────────────────────────────────────────────────┼──────┼───────────────────────────────────────────┤
  │ 16  │ Customer.tasks(uuid)                         │ GET /tasks?customer_uuid=…                     │ 200  │ ✅                                        │
  ├─────┼──────────────────────────────────────────────┼────────────────────────────────────────────────┼──────┼───────────────────────────────────────────┤
  │ 17  │ CustomerNote.all(…) (deprecated class)       │ GET /customer_notes?customer_uuid=…            │ 200  │ ✅ DeprecationWarning emitted             │
  ├─────┼──────────────────────────────────────────────┼────────────────────────────────────────────────┼──────┼───────────────────────────────────────────┤
  │ 18  │ Contact.create(data={…}) standalone          │ POST /contacts                                 │ 201  │ ✅ customer_uuid=None                     │
  ├─────┼──────────────────────────────────────────────┼────────────────────────────────────────────────┼──────┼───────────────────────────────────────────┤
  │ 19  │ Contact.all(email=…)                         │ GET /contacts?email=…                          │ 200  │ ✅ 1 contact                              │
  ├─────┼──────────────────────────────────────────────┼────────────────────────────────────────────────┼──────┼───────────────────────────────────────────┤
  │ 20  │ Contact.all(customer_external_id=…)          │ GET /contacts?customer_external_id=id-1        │ 200  │ ✅ filter applied (0 — no linked contact) │
  ├─────┼──────────────────────────────────────────────┼────────────────────────────────────────────────┼──────┼───────────────────────────────────────────┤
  │ 21  │ Contact.all(external_id=…)                   │ GET /contacts?external_id=…                    │ 200  │ ✅ filter applied                         │
  ├─────┼──────────────────────────────────────────────┼────────────────────────────────────────────────┼──────┼───────────────────────────────────────────┤
  │ 22  │ Contact.createEntityNote(uuid, data)         │ POST /notes                                    │ 201  │ ✅ assoc_obj=contact injected             │
  ├─────┼──────────────────────────────────────────────┼────────────────────────────────────────────────┼──────┼───────────────────────────────────────────┤
  │ 23  │ Contact.entityNotes(uuid)                    │ GET /notes?contact_uuid=…                      │ 200  │ ✅                                        │
  ├─────┼──────────────────────────────────────────────┼────────────────────────────────────────────────┼──────┼───────────────────────────────────────────┤
  │ 24  │ Contact.createTask(uuid, data)               │ POST /tasks                                    │ 201  │ ✅ assoc_obj=contact injected             │
  ├─────┼──────────────────────────────────────────────┼────────────────────────────────────────────────┼──────┼───────────────────────────────────────────┤
  │ 25  │ Contact.tasks(uuid)                          │ GET /tasks?contact_uuid=…                      │ 200  │ ✅                                        │
  ├─────┼──────────────────────────────────────────────┼────────────────────────────────────────────────┼──────┼───────────────────────────────────────────┤
  │ 26  │ Task.create(…, associated_object_identifier) │ POST /tasks                                    │ 201  │ ✅ explicit identifier path               │
  ├─────┼──────────────────────────────────────────────┼────────────────────────────────────────────────┼──────┼───────────────────────────────────────────┤
  │ 27  │ EntityNote.destroy(uuid)                     │ DELETE /notes/{uuid}                           │ 202  │ ✅                                        │
  ├─────┼──────────────────────────────────────────────┼────────────────────────────────────────────────┼──────┼───────────────────────────────────────────┤
  │ 28  │ Task.destroy(uuid)                           │ DELETE /tasks/{uuid}                           │ 202  │ ✅                                        │
  ├─────┼──────────────────────────────────────────────┼────────────────────────────────────────────────┼──────┼───────────────────────────────────────────┤
  │ 29  │ Contact.create(external_id=…) (positive)     │ POST /contacts                                 │ 201  │ ✅                                        │
  ├─────┼──────────────────────────────────────────────┼────────────────────────────────────────────────┼──────┼───────────────────────────────────────────┤
  │ 30  │ Contact.all(external_id=…) (positive)        │ GET /contacts?external_id=…                    │ 200  │ ✅ returns exactly the 1 match            │
  └─────┴──────────────────────────────────────────────┴────────────────────────────────────────────────┴──────┴───────────────────────────────────────────┘


@keith-chartmogul

Copy link
Copy Markdown
Contributor Author

Nice, It requires contact_profiles feature flag, it should be fully rolled out, right?

@ya-petrov-evgeniy I will not deploy these SDK library changes until the feature flag has been enabled for all customers, so I am assuming this is the case with these library updates. The feature has not been released in platform yet, so any approvals in these SDKs will hold until that has been completed. 👍

@briwa briwa left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍 Next time, I think you could tag the analytics/data team for a review, since they primarily use Python in day-to-day work

@keith-chartmogul
keith-chartmogul merged commit cf048a9 into main Oct 12, 2026
9 checks passed
@keith-chartmogul
keith-chartmogul deleted the align_changes_to_contacts_tasks_notes_in_public_api branch October 12, 2026 11:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants