From 9fb6c99d3beff1501f109a34f23b81c8f0f52d7c Mon Sep 17 00:00:00 2001 From: sdk-sync Date: Tue, 13 Oct 2026 18:20:59 -0400 Subject: [PATCH] I added `CustomerIO.entity(**kwargs)` to `customerio/track.py`. It sends the keyword arguments as one operation to `/api/v2/entity` using a POST request, and it builds the URL the same way `batch()` does, including the host and port handling. If you call it with no arguments, it raises `CustomerIOException`, just as the node `entity()` rejects an empty operation. I added two tests, `test_entity_call` and `test_entity_empty_raises`, written like the existing batch test. All 62 tests pass. --- customerio/track.py | 16 ++++++++++++++++ tests/test_customerio.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/customerio/track.py b/customerio/track.py index 514bf82..e2ff671 100644 --- a/customerio/track.py +++ b/customerio/track.py @@ -264,6 +264,22 @@ def batch(self, operations): url = f"https://{self.host}:{self.port}/api/v2/batch" return self.send_request("POST", url, {"batch": operations}) + def entity(self, **kwargs): + """Send a single operation to the v2 entity endpoint. + + This is the singular counterpart to `batch`: the keyword arguments form + one batch-style operation (e.g. type, action, identifiers, attributes). + See https://customer.io/docs/api/track/#operation/entity + """ + if not kwargs: + raise CustomerIOException("operation cannot be empty in entity") + + if self.port == 443: + url = f"https://{self.host}/api/v2/entity" + else: + url = f"https://{self.host}:{self.port}/api/v2/entity" + return self.send_request("POST", url, kwargs) + def _build_session(self): session = super()._build_session() session.auth = (self.site_id, self.api_key) diff --git a/tests/test_customerio.py b/tests/test_customerio.py index 157ac18..3ed32f1 100644 --- a/tests/test_customerio.py +++ b/tests/test_customerio.py @@ -852,6 +852,36 @@ def test_batch_call(self): with self.assertRaises(CustomerIOException): self.cio.batch(None) + def test_entity_call(self): + self.cio.http.hooks = dict( + response=partial( + self._check_request, + rq={ + "method": "POST", + "authorization": _basic_auth_str("siteid", "apikey"), + "content_type": "application/json", + "url_suffix": "/api/v2/entity", + "body": { + "type": "person", + "action": "identify", + "identifiers": {"id": "1"}, + "attributes": {"name": "Alice"}, + }, + }, + ) + ) + + self.cio.entity( + type="person", + action="identify", + identifiers={"id": "1"}, + attributes={"name": "Alice"}, + ) + + def test_entity_empty_raises(self): + with self.assertRaises(CustomerIOException): + self.cio.entity() + if __name__ == "__main__": unittest.main()