diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b3aaadbc..3e38b5e1e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,7 +54,7 @@ jobs: - name: Install requirements.txt dependencies with pip run: | - python -m pip install -e . + python -m pip install -e .[test] - name: Run posthog tests run: | diff --git a/.gitignore b/.gitignore index af659a804..ee8b53534 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ flake8.out pylint.out posthog-analytics .idea +.python-version +.coverage \ No newline at end of file diff --git a/Makefile b/Makefile index 1a3a9fd58..c02f3f2f8 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,10 @@ -test: +lint: pylint --rcfile=.pylintrc --reports=y --exit-zero analytics | tee pylint.out flake8 --max-complexity=10 --statistics analytics > flake8.out || true - coverage run --branch --include=analytics/\* --omit=*/test* setup.py test + +test: + coverage run -m pytest + coverage report release: rm -rf dist/* @@ -26,4 +29,4 @@ release_analytics: e2e_test: .buildscripts/e2e.sh -.PHONY: test release e2e_test +.PHONY: test lint release e2e_test diff --git a/README.md b/README.md index 8ec142763..deee1ef11 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,16 @@ Specifically, the [Python integration](https://posthog.com/docs/integrations/pyt ## Questions? ### [Join our Slack community.](https://join.slack.com/t/posthogusers/shared_invite/enQtOTY0MzU5NjAwMDY3LTc2MWQ0OTZlNjhkODk3ZDI3NDVjMDE1YjgxY2I4ZjI4MzJhZmVmNjJkN2NmMGJmMzc2N2U3Yjc3ZjI5NGFlZDQ) + +# Local Development + +## Testing Locally + +1. Run `python3 -m venv env` (creates virtual environment called "env") +2. Run `source env/bin/activate` (activates the virtual environment) +3. Run `python3 -m pip install -e ".[test]"` (installs the package in develop mode, along with test dependencies) +4. Run `make test` + +## Running Locally + +Assuming you have a [local version of PostHog](https://posthog.com/docs/developing-locally) running, you can run `python3 example.py` to see the library in action. \ No newline at end of file diff --git a/example.py b/example.py index af3e346d7..c47071f25 100644 --- a/example.py +++ b/example.py @@ -19,12 +19,29 @@ print(posthog.feature_enabled("beta-feature", "distinct_id")) print("sleeping") -time.sleep(45) +time.sleep(5) print(posthog.feature_enabled("beta-feature", "distinct_id")) # # Alias a previous distinct id with a new one + posthog.alias("distinct_id", "new_distinct_id") +posthog.capture("new_distinct_id", "event2", {"property1": "value", "property2": "value"}) + # # Add properties to the person -posthog.identify("distinct_id", {"email": "something@something.com"}) +posthog.identify("new_distinct_id", {"email": "something@something.com"}) + +# properties set only once to the person +posthog.set_once("new_distinct_id", {"self_serve_signup": True}) + +time.sleep(3) + +posthog.set_once( + "new_distinct_id", {"self_serve_signup": False} +) # this will not change the property (because it was already set) + +posthog.set("new_distinct_id", {"current_browser": "Chrome"}) +posthog.set("new_distinct_id", {"current_browser": "Firefox"}) + +posthog.shutdown() diff --git a/posthog/__init__.py b/posthog/__init__.py index 9f08630f1..0894139dc 100644 --- a/posthog/__init__.py +++ b/posthog/__init__.py @@ -32,7 +32,7 @@ def capture( A `capture` call requires - `distinct id` which uniquely identifies your user - - `event name` to make sure + - `event name` to specify the event - We recommend using [verb] [noun], like `movie played` or `movie updated` to easily identify what your events mean later on. Optionally you can submit @@ -87,6 +87,72 @@ def identify( ) +def set( + distinct_id, # type: str, + properties=None, # type: Optional[Dict] + context=None, # type: Optional[Dict] + timestamp=None, # type: Optional[datetime.datetime] + message_id=None, # type: Optional[str] +): + # type: (...) -> None + """ + Set properties on a user record. + This will overwrite previous people property values, just like `identify`. + + A `set` call requires + - `distinct id` which uniquely identifies your user + - `properties` with a dict with any key: value pairs + + For example: + ```python + posthog.set('distinct id', { + 'current_browser': 'Chrome', + }) + ``` + """ + _proxy( + "set", + distinct_id=distinct_id, + properties=properties, + context=context, + timestamp=timestamp, + message_id=message_id, + ) + + +def set_once( + distinct_id, # type: str, + properties=None, # type: Optional[Dict] + context=None, # type: Optional[Dict] + timestamp=None, # type: Optional[datetime.datetime] + message_id=None, # type: Optional[str] +): + # type: (...) -> None + """ + Set properties on a user record, only if they do not yet exist. + This will not overwrite previous people property values, unlike `identify`. + + A `set_once` call requires + - `distinct id` which uniquely identifies your user + - `properties` with a dict with any key: value pairs + + For example: + ```python + posthog.set_once('distinct id', { + 'referred_by': 'friend', + }) + ``` + """ + _proxy( + "set_once", + distinct_id=distinct_id, + properties=properties, + context=context, + timestamp=timestamp, + message_id=message_id, + ) + + def group(*args, **kwargs): """Send a group call.""" _proxy("group", *args, **kwargs) diff --git a/posthog/client.py b/posthog/client.py index be076c830..f7d42472d 100644 --- a/posthog/client.py +++ b/posthog/client.py @@ -137,6 +137,40 @@ def capture(self, distinct_id=None, event=None, properties=None, context=None, t return self._enqueue(msg) + def set(self, distinct_id=None, properties=None, context=None, timestamp=None, message_id=None): + properties = properties or {} + context = context or {} + require("distinct_id", distinct_id, ID_TYPES) + require("properties", properties, dict) + + msg = { + "timestamp": timestamp, + "context": context, + "distinct_id": distinct_id, + "$set": properties, + "event": "$set", + "messageId": message_id, + } + + return self._enqueue(msg) + + def set_once(self, distinct_id=None, properties=None, context=None, timestamp=None, message_id=None): + properties = properties or {} + context = context or {} + require("distinct_id", distinct_id, ID_TYPES) + require("properties", properties, dict) + + msg = { + "timestamp": timestamp, + "context": context, + "distinct_id": distinct_id, + "$set_once": properties, + "event": "$set_once", + "messageId": message_id, + } + + return self._enqueue(msg) + def alias(self, previous_id=None, distinct_id=None, context=None, timestamp=None, message_id=None): context = context or {} diff --git a/posthog/consumer.py b/posthog/consumer.py index 8813b1b61..e5e4acfd3 100644 --- a/posthog/consumer.py +++ b/posthog/consumer.py @@ -117,7 +117,7 @@ def next(self): return items def request(self, batch): - """Attempt to upload the batch and retry before raising an error """ + """Attempt to upload the batch and retry before raising an error""" def fatal_exception(exc): if isinstance(exc, APIError): diff --git a/posthog/test/client.py b/posthog/test/test_client.py similarity index 83% rename from posthog/test/client.py rename to posthog/test/test_client.py index 79beb47b0..3fac238c2 100644 --- a/posthog/test/client.py +++ b/posthog/test/test_client.py @@ -9,7 +9,7 @@ from posthog.client import Client from posthog.request import APIError -from posthog.test.utils import TEST_API_KEY +from posthog.test.test_utils import TEST_API_KEY from posthog.version import VERSION @@ -105,6 +105,64 @@ def test_advanced_identify(self): self.assertEqual(msg["messageId"], "messageId") self.assertEqual(msg["distinct_id"], "distinct_id") + def test_basic_set(self): + client = self.client + success, msg = client.set("distinct_id", {"trait": "value"}) + client.flush() + self.assertTrue(success) + self.assertFalse(self.failed) + + self.assertEqual(msg["$set"]["trait"], "value") + self.assertTrue(isinstance(msg["timestamp"], str)) + self.assertTrue(isinstance(msg["messageId"], str)) + self.assertEqual(msg["distinct_id"], "distinct_id") + + def test_advanced_set(self): + client = self.client + success, msg = client.set( + "distinct_id", {"trait": "value"}, {"ip": "192.168.0.1"}, datetime(2014, 9, 3), "messageId" + ) + + self.assertTrue(success) + + self.assertEqual(msg["timestamp"], "2014-09-03T00:00:00+00:00") + self.assertEqual(msg["context"]["ip"], "192.168.0.1") + self.assertEqual(msg["$set"]["trait"], "value") + self.assertEqual(msg["properties"]["$lib"], "posthog-python") + self.assertEqual(msg["properties"]["$lib_version"], VERSION) + self.assertTrue(isinstance(msg["timestamp"], str)) + self.assertEqual(msg["messageId"], "messageId") + self.assertEqual(msg["distinct_id"], "distinct_id") + + def test_basic_set_once(self): + client = self.client + success, msg = client.set_once("distinct_id", {"trait": "value"}) + client.flush() + self.assertTrue(success) + self.assertFalse(self.failed) + + self.assertEqual(msg["$set_once"]["trait"], "value") + self.assertTrue(isinstance(msg["timestamp"], str)) + self.assertTrue(isinstance(msg["messageId"], str)) + self.assertEqual(msg["distinct_id"], "distinct_id") + + def test_advanced_set_once(self): + client = self.client + success, msg = client.set_once( + "distinct_id", {"trait": "value"}, {"ip": "192.168.0.1"}, datetime(2014, 9, 3), "messageId" + ) + + self.assertTrue(success) + + self.assertEqual(msg["timestamp"], "2014-09-03T00:00:00+00:00") + self.assertEqual(msg["context"]["ip"], "192.168.0.1") + self.assertEqual(msg["$set_once"]["trait"], "value") + self.assertEqual(msg["properties"]["$lib"], "posthog-python") + self.assertEqual(msg["properties"]["$lib_version"], VERSION) + self.assertTrue(isinstance(msg["timestamp"], str)) + self.assertEqual(msg["messageId"], "messageId") + self.assertEqual(msg["distinct_id"], "distinct_id") + def test_basic_alias(self): client = self.client success, msg = client.alias("previousId", "distinct_id") @@ -125,7 +183,7 @@ def test_basic_page(self): def test_basic_page_distinct_uuid(self): client = self.client - distinct_id = uuid4() + distinct_id = str(uuid4()) success, msg = client.page(distinct_id, url="https://posthog.com/contact") self.assertFalse(self.failed) client.flush() diff --git a/posthog/test/consumer.py b/posthog/test/test_consumer.py similarity index 99% rename from posthog/test/consumer.py rename to posthog/test/test_consumer.py index 49ee7d936..1747f6d3e 100644 --- a/posthog/test/consumer.py +++ b/posthog/test/test_consumer.py @@ -11,7 +11,7 @@ from posthog.consumer import MAX_MSG_SIZE, Consumer from posthog.request import APIError -from posthog.test.utils import TEST_API_KEY +from posthog.test.test_utils import TEST_API_KEY class TestConsumer(unittest.TestCase): diff --git a/posthog/test/module.py b/posthog/test/test_module.py similarity index 100% rename from posthog/test/module.py rename to posthog/test/test_module.py diff --git a/posthog/test/request.py b/posthog/test/test_request.py similarity index 97% rename from posthog/test/request.py rename to posthog/test/test_request.py index ef1fa0c59..db4818309 100644 --- a/posthog/test/request.py +++ b/posthog/test/test_request.py @@ -5,7 +5,7 @@ import requests from posthog.request import DatetimeSerializer, batch_post -from posthog.test.utils import TEST_API_KEY +from posthog.test.test_utils import TEST_API_KEY class TestRequests(unittest.TestCase): diff --git a/posthog/test/utils.py b/posthog/test/test_utils.py similarity index 100% rename from posthog/test/utils.py rename to posthog/test/test_utils.py diff --git a/setup.py b/setup.py index c4a947370..a8aa8d119 100644 --- a/setup.py +++ b/setup.py @@ -21,11 +21,10 @@ "black", "isort", "pre-commit", - ] + ], + "test": ["mock>=2.0.0", "freezegun==0.3.15", "pylint", "flake8", "coverage"], } -tests_require = ["mock>=2.0.0", "freezegun==0.3.15"] - setup( name="posthog", version=VERSION, @@ -39,7 +38,6 @@ license="MIT License", install_requires=install_requires, extras_require=extras_require, - tests_require=tests_require, description="Integrate PostHog into any python application.", long_description=long_description, classifiers=[ diff --git a/simulator.py b/simulator.py index 2602659a9..9c77a90d5 100644 --- a/simulator.py +++ b/simulator.py @@ -71,6 +71,22 @@ def identify(): ) +def set_once(): + posthog.set_once( + options.distinct_id, + properties=json_hash(options.traits), + context=json_hash(options.context), + ) + + +def set(): + posthog.set( + options.distinct_id, + properties=json_hash(options.traits), + context=json_hash(options.context), + ) + + def unknown(): print() @@ -84,7 +100,7 @@ def unknown(): ch.setLevel(logging.DEBUG) log.addHandler(ch) -switcher = {"capture": capture, "page": page, "identify": identify} +switcher = {"capture": capture, "page": page, "identify": identify, "set_once": set_once, "set": set} func = switcher.get(options.type) if func: